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 node: true
}, },
extends: [ extends: [
'plugin:vue/vue3-essential', "plugin:vue/vue3-essential",
'@vue/standard', "@vue/standard",
'@vue/typescript/recommended' "@vue/typescript/recommended"
], ],
parserOptions: { parserOptions: {
ecmaVersion: 2020 ecmaVersion: 2020
}, },
globals: {
defineProps: "readonly",
defineEmits: "readonly",
defineExpose: "readonly",
withDefaults: "readonly"
},
rules: { rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
'vue/no-multiple-template-root': '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": { "@babel/helper-validator-identifier": {
"version": "7.14.5", "version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", "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": { "@babel/helper-validator-option": {
"version": "7.14.5", "version": "7.14.5",
@ -309,7 +310,8 @@
"@babel/parser": { "@babel/parser": {
"version": "7.14.7", "version": "7.14.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", "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": { "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
"version": "7.14.5", "version": "7.14.5",
@ -1096,6 +1098,7 @@
"version": "7.14.5", "version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz",
"integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==",
"dev": true,
"requires": { "requires": {
"@babel/helper-validator-identifier": "^7.14.5", "@babel/helper-validator-identifier": "^7.14.5",
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
@ -1361,9 +1364,9 @@
} }
}, },
"@types/bootstrap": { "@types/bootstrap": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.2.tgz",
"integrity": "sha512-W/fEBlqwaJFh+3sCz/H88LPsLt/zLsEECFlrAOkrRPjWuo/ETl8u0JefIerCdc8+WukowQS1f60eIJOwkCBwhg==", "integrity": "sha512-dSQvMi2dMyNwJU6LZjP0pimuBowsMUvGScYdfqqeiDUoj9TxXZCpfu0cTl94U0Zvw/tdH9j/9ToOhi4LKNLZhg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.9.2",
@ -1389,6 +1392,15 @@
"@types/node": "*" "@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": { "@types/estree": {
"version": "0.0.48", "version": "0.0.48",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz",
@ -1452,10 +1464,16 @@
"integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==", "integrity": "sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg==",
"dev": true "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": { "@types/marked": {
"version": "2.0.4", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-2.0.5.tgz",
"integrity": "sha512-L9VRSe0Id8xbPL99mUo/4aKgD7ZoRwFZqUQScNKHi2pFjF9ZYSMNShUHD6VlMT6J/prQq0T1mxuU25m3R7dFzg==", "integrity": "sha512-shRZ7XnYFD/8n8zSjKvFdto1QNSf4tONZIlNEZGrJe8GsOE8DL/hG1Hbl8gZlfLnjS7+f5tZGIaTgfpyW38h4w==",
"dev": true "dev": true
}, },
"@types/mime": { "@types/mime": {
@ -1541,6 +1559,12 @@
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==",
"dev": true "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": { "@types/uglify-js": {
"version": "3.13.1", "version": "3.13.1",
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz",
@ -1619,13 +1643,13 @@
} }
}, },
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "4.29.1", "version": "4.29.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.3.tgz",
"integrity": "sha512-AHqIU+SqZZgBEiWOrtN94ldR3ZUABV5dUG94j8Nms9rQnHFc8fvDOue/58K4CFz6r8OtDDc35Pw9NQPWo0Ayrw==", "integrity": "sha512-tBgfA3K/3TsZY46ROGvoRxQr1wBkclbVqRQep97MjVHJzcRBURRY3sNFqLk0/Xr//BY5hM9H2p/kp+6qim85SA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/experimental-utils": "4.29.1", "@typescript-eslint/experimental-utils": "4.29.3",
"@typescript-eslint/scope-manager": "4.29.1", "@typescript-eslint/scope-manager": "4.29.3",
"debug": "^4.3.1", "debug": "^4.3.1",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"regexpp": "^3.1.0", "regexpp": "^3.1.0",
@ -1669,15 +1693,15 @@
} }
}, },
"@typescript-eslint/experimental-utils": { "@typescript-eslint/experimental-utils": {
"version": "4.29.1", "version": "4.29.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.3.tgz",
"integrity": "sha512-kl6QG6qpzZthfd2bzPNSJB2YcZpNOrP6r9jueXupcZHnL74WiuSjaft7WSu17J9+ae9zTlk0KJMXPUj0daBxMw==", "integrity": "sha512-ffIvbytTVWz+3keg+Sy94FG1QeOvmV9dP2YSdLFHw/ieLXWCa3U1TYu8IRCOpMv2/SPS8XqhM1+ou1YHsdzKrg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/json-schema": "^7.0.7", "@types/json-schema": "^7.0.7",
"@typescript-eslint/scope-manager": "4.29.1", "@typescript-eslint/scope-manager": "4.29.3",
"@typescript-eslint/types": "4.29.1", "@typescript-eslint/types": "4.29.3",
"@typescript-eslint/typescript-estree": "4.29.1", "@typescript-eslint/typescript-estree": "4.29.3",
"eslint-scope": "^5.1.1", "eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0" "eslint-utils": "^3.0.0"
}, },
@ -1695,41 +1719,41 @@
} }
}, },
"@typescript-eslint/parser": { "@typescript-eslint/parser": {
"version": "4.29.1", "version": "4.29.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.29.3.tgz",
"integrity": "sha512-3fL5iN20hzX3Q4OkG7QEPFjZV2qsVGiDhEwwh+EkmE/w7oteiOvUNzmpu5eSwGJX/anCryONltJ3WDmAzAoCMg==", "integrity": "sha512-jrHOV5g2u8ROghmspKoW7pN8T/qUzk0+DITun0MELptvngtMrwUJ1tv5zMI04CYVEUsSrN4jV7AKSv+I0y0EfQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/scope-manager": "4.29.1", "@typescript-eslint/scope-manager": "4.29.3",
"@typescript-eslint/types": "4.29.1", "@typescript-eslint/types": "4.29.3",
"@typescript-eslint/typescript-estree": "4.29.1", "@typescript-eslint/typescript-estree": "4.29.3",
"debug": "^4.3.1" "debug": "^4.3.1"
} }
}, },
"@typescript-eslint/scope-manager": { "@typescript-eslint/scope-manager": {
"version": "4.29.1", "version": "4.29.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.29.3.tgz",
"integrity": "sha512-Hzv/uZOa9zrD/W5mftZa54Jd5Fed3tL6b4HeaOpwVSabJK8CJ+2MkDasnX/XK4rqP5ZTWngK1ZDeCi6EnxPQ7A==", "integrity": "sha512-x+w8BLXO7iWPkG5mEy9bA1iFRnk36p/goVlYobVWHyDw69YmaH9q6eA+Fgl7kYHmFvWlebUTUfhtIg4zbbl8PA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "4.29.1", "@typescript-eslint/types": "4.29.3",
"@typescript-eslint/visitor-keys": "4.29.1" "@typescript-eslint/visitor-keys": "4.29.3"
} }
}, },
"@typescript-eslint/types": { "@typescript-eslint/types": {
"version": "4.29.1", "version": "4.29.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.29.3.tgz",
"integrity": "sha512-Jj2yu78IRfw4nlaLtKjVaGaxh/6FhofmQ/j8v3NXmAiKafbIqtAPnKYrf0sbGjKdj0hS316J8WhnGnErbJ4RCA==", "integrity": "sha512-s1eV1lKNgoIYLAl1JUba8NhULmf+jOmmeFO1G5MN/RBCyyzg4TIOfIOICVNC06lor+Xmy4FypIIhFiJXOknhIg==",
"dev": true "dev": true
}, },
"@typescript-eslint/typescript-estree": { "@typescript-eslint/typescript-estree": {
"version": "4.29.1", "version": "4.29.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.3.tgz",
"integrity": "sha512-lIkkrR9E4lwZkzPiRDNq0xdC3f2iVCUjw/7WPJ4S2Sl6C3nRWkeE1YXCQ0+KsiaQRbpY16jNaokdWnm9aUIsfw==", "integrity": "sha512-45oQJA0bxna4O5TMwz55/TpgjX1YrAPOI/rb6kPgmdnemRZx/dB0rsx+Ku8jpDvqTxcE1C/qEbVHbS3h0hflag==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "4.29.1", "@typescript-eslint/types": "4.29.3",
"@typescript-eslint/visitor-keys": "4.29.1", "@typescript-eslint/visitor-keys": "4.29.3",
"debug": "^4.3.1", "debug": "^4.3.1",
"globby": "^11.0.3", "globby": "^11.0.3",
"is-glob": "^4.0.1", "is-glob": "^4.0.1",
@ -1882,12 +1906,12 @@
} }
}, },
"@typescript-eslint/visitor-keys": { "@typescript-eslint/visitor-keys": {
"version": "4.29.1", "version": "4.29.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.3.tgz",
"integrity": "sha512-zLqtjMoXvgdZY/PG6gqA73V8BjqPs4af1v2kiiETBObp+uC6gRYnJLmJHxC0QyUrrHDLJPIWNYxoBV3wbcRlag==", "integrity": "sha512-MGGfJvXT4asUTeVs0Q2m+sY63UsfnA+C/FDgBKV3itLBmM9H0u+URcneePtkd0at1YELmZK6HSolCqM4Fzs6yA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "4.29.1", "@typescript-eslint/types": "4.29.3",
"eslint-visitor-keys": "^2.0.0" "eslint-visitor-keys": "^2.0.0"
} }
}, },
@ -2457,17 +2481,36 @@
} }
}, },
"@vue/compiler-core": { "@vue/compiler-core": {
"version": "3.2.2", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.6.tgz",
"integrity": "sha512-QhCI0ZU5nAR0LMcLgzW3v75374tIrHGp8XG5CzJS7Nsy+iuignbE4MZ2XJfh5TGIrtpuzfWA4eTIfukZf/cRdg==", "integrity": "sha512-vbwnz7+OhtLO5p5i630fTuQCL+MlUpEMTKHuX+RfetQ+3pFCkItt2JUH+9yMaBG2Hkz6av+T9mwN/acvtIwpbw==",
"requires": { "requires": {
"@babel/parser": "^7.12.0", "@babel/parser": "^7.15.0",
"@babel/types": "^7.12.0", "@babel/types": "^7.15.0",
"@vue/shared": "3.2.2", "@vue/shared": "3.2.6",
"estree-walker": "^2.0.1", "estree-walker": "^2.0.2",
"source-map": "^0.6.1" "source-map": "^0.6.1"
}, },
"dependencies": { "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": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -2476,29 +2519,30 @@
} }
}, },
"@vue/compiler-dom": { "@vue/compiler-dom": {
"version": "3.2.2", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.6.tgz",
"integrity": "sha512-ggcc+NV/ENIE0Uc3TxVE/sKrhYVpLepMAAmEiQ047332mbKOvUkowz4TTFZ+YkgOIuBOPP0XpCxmCMg7p874mA==", "integrity": "sha512-+a/3oBAzFIXhHt8L5IHJOTP4a5egzvpXYyi13jR7CUYOR1S+Zzv7vBWKYBnKyJLwnrxTZnTQVjeHCgJq743XKg==",
"requires": { "requires": {
"@vue/compiler-core": "3.2.2", "@vue/compiler-core": "3.2.6",
"@vue/shared": "3.2.2" "@vue/shared": "3.2.6"
} }
}, },
"@vue/compiler-sfc": { "@vue/compiler-sfc": {
"version": "3.2.2", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.6.tgz",
"integrity": "sha512-hrtqpQ5L6IPn5v7yVRo7uvLcQxv0z1+KBjZBWMBOcrXz4t+PKUxU/SWd6Tl9T8FDmYlunzKUh6lcx+2CLo6f5A==", "integrity": "sha512-Ariz1eDsf+2fw6oWXVwnBNtfKHav72RjlWXpEgozYBLnfRPzP+7jhJRw4Nq0OjSsLx2HqjF3QX7HutTjYB0/eA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/parser": "^7.13.9", "@babel/parser": "^7.15.0",
"@babel/types": "^7.13.0", "@babel/types": "^7.15.0",
"@types/estree": "^0.0.48", "@types/estree": "^0.0.48",
"@vue/compiler-core": "3.2.2", "@vue/compiler-core": "3.2.6",
"@vue/compiler-dom": "3.2.2", "@vue/compiler-dom": "3.2.6",
"@vue/compiler-ssr": "3.2.2", "@vue/compiler-ssr": "3.2.6",
"@vue/shared": "3.2.2", "@vue/ref-transform": "3.2.6",
"@vue/shared": "3.2.6",
"consolidate": "^0.16.0", "consolidate": "^0.16.0",
"estree-walker": "^2.0.1", "estree-walker": "^2.0.2",
"hash-sum": "^2.0.0", "hash-sum": "^2.0.0",
"lru-cache": "^5.1.1", "lru-cache": "^5.1.1",
"magic-string": "^0.25.7", "magic-string": "^0.25.7",
@ -2509,6 +2553,28 @@
"source-map": "^0.6.1" "source-map": "^0.6.1"
}, },
"dependencies": { "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": { "consolidate": {
"version": "0.16.0", "version": "0.16.0",
"resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.16.0.tgz", "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.16.0.tgz",
@ -2538,13 +2604,13 @@
} }
}, },
"@vue/compiler-ssr": { "@vue/compiler-ssr": {
"version": "3.2.2", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.6.tgz",
"integrity": "sha512-rVl1agMFhdEN3Go0bCriXo+3cysxKIuRP0yh1Wd8ysRrKfAmokyDhUA8PrGSq2Ymj/LdZTh+4OKfj3p2+C+hlA==", "integrity": "sha512-A7IKRKHSyPnTC4w1FxHkjzoyjXInsXkcs/oX22nBQ+6AWlXj2Tt1le96CWPOXy5vYlsTYkF1IgfBaKIdeN/39g==",
"dev": true, "dev": true,
"requires": { "requires": {
"@vue/compiler-dom": "3.2.2", "@vue/compiler-dom": "3.2.6",
"@vue/shared": "3.2.2" "@vue/shared": "3.2.6"
} }
}, },
"@vue/component-compiler-utils": { "@vue/component-compiler-utils": {
@ -2626,36 +2692,57 @@
"dev": true "dev": true
}, },
"@vue/reactivity": { "@vue/reactivity": {
"version": "3.2.2", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.6.tgz",
"integrity": "sha512-IHjhtmrhK6dzacj/EnLQDWOaA3HuzzVk6w84qgV8EpS4uWGIJXiRalMRg6XvGW2ykJvIl3pLsF0aBFlTMRiLOA==", "integrity": "sha512-8vIDD2wpCnYisNNZjmcIj+Rixn0uhZNY3G1vzlgdVdLygeRSuFjkmnZk6WwvGzUWpKfnG0e/NUySM3mVi59hAA==",
"requires": { "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": { "@vue/runtime-core": {
"version": "3.2.2", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.6.tgz",
"integrity": "sha512-/aUk1+GO/VPX0oVxhbzSWE1zrf3/wGCsO1ALNisVokYftKqfqLDjbJHE6mrI2hx3MiuwbHrWjJClkGUVTIOPEQ==", "integrity": "sha512-3mqtgpj/YSGFxtvTufSERRApo92B16JNNxz9p+5eG6PPuqTmuRJz214MqhKBEgLEAIQ6R6YCbd83ZDtjQnyw2g==",
"requires": { "requires": {
"@vue/reactivity": "3.2.2", "@vue/reactivity": "3.2.6",
"@vue/shared": "3.2.2" "@vue/shared": "3.2.6"
} }
}, },
"@vue/runtime-dom": { "@vue/runtime-dom": {
"version": "3.2.2", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.6.tgz",
"integrity": "sha512-1Le/NpCfawCOfePfJezvWUF+oCVLU8N+IHN4oFDOxRe6/PgHNJ+yT+YdxFifBfI+TIAoXI/9PsnqzmJZV+xsmw==", "integrity": "sha512-fq33urnP0BNCGm2O3KCzkJlKIHI80C94HJ4qDZbjsTtxyOn5IHqwKSqXVN3RQvO6epcQH+sWS+JNwcNDPzoasg==",
"requires": { "requires": {
"@vue/runtime-core": "3.2.2", "@vue/runtime-core": "3.2.6",
"@vue/shared": "3.2.2", "@vue/shared": "3.2.6",
"csstype": "^2.6.8" "csstype": "^2.6.8"
} }
}, },
"@vue/shared": { "@vue/shared": {
"version": "3.2.2", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.6.tgz",
"integrity": "sha512-dvYb318tk9uOzHtSaT3WII/HscQSIRzoCZ5GyxEb3JlkEXASpAUAQwKnvSe2CudnF8XHFRTB7VITWSnWNLZUtA==" "integrity": "sha512-uwX0Qs2e6kdF+WmxwuxJxOnKs/wEkMArtYpHSm7W+VY/23Tl8syMRyjnzEeXrNCAP0/8HZxEGkHJsjPEDNRuHw=="
}, },
"@vue/web-component-wrapper": { "@vue/web-component-wrapper": {
"version": "1.3.0", "version": "1.3.0",
@ -3086,6 +3173,12 @@
"es-abstract": "^1.18.0-next.1" "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": { "asn1": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "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": { "assert-plus": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "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/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": { "balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -3891,6 +3999,15 @@
"supports-color": "^5.3.0" "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": { "chardet": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
@ -4359,6 +4476,16 @@
"bluebird": "^3.1.1" "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": { "constants-browserify": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
@ -4580,9 +4707,9 @@
} }
}, },
"core-js": { "core-js": {
"version": "3.16.1", "version": "3.16.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.1.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.3.tgz",
"integrity": "sha512-AAkP8i35EbefU+JddyWi12AWE9f2N/qr/pwnDtWz4nyUIBGMJPX99ANFFRSw6FefM374lDujdtLDyhN2A/btHw==" "integrity": "sha512-lM3GftxzHNtPNUJg0v4pC2RC6puwMd6VZA7vXUczi+SKmCWSf4JwO89VJGMqbzmB7jlK7B5hr3S64PqwFL49cA=="
}, },
"core-js-compat": { "core-js-compat": {
"version": "3.15.2", "version": "3.15.2",
@ -5351,6 +5478,12 @@
"esutils": "^2.0.2" "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": { "dom-converter": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", "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": { "domutils": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
@ -6118,26 +6256,26 @@
} }
}, },
"eslint-plugin-import": { "eslint-plugin-import": {
"version": "2.24.0", "version": "2.24.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz",
"integrity": "sha512-Kc6xqT9hiYi2cgybOc0I2vC9OgAYga5o/rAFinam/yF/t5uBqxQbauNPMC6fgb640T/89P0gFoO27FOilJ/Cqg==", "integrity": "sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q==",
"dev": true, "dev": true,
"requires": { "requires": {
"array-includes": "^3.1.3", "array-includes": "^3.1.3",
"array.prototype.flat": "^1.2.4", "array.prototype.flat": "^1.2.4",
"debug": "^2.6.9", "debug": "^2.6.9",
"doctrine": "^2.1.0", "doctrine": "^2.1.0",
"eslint-import-resolver-node": "^0.3.5", "eslint-import-resolver-node": "^0.3.6",
"eslint-module-utils": "^2.6.2", "eslint-module-utils": "^2.6.2",
"find-up": "^2.0.0", "find-up": "^2.0.0",
"has": "^1.0.3", "has": "^1.0.3",
"is-core-module": "^2.4.0", "is-core-module": "^2.6.0",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"object.values": "^1.1.3", "object.values": "^1.1.4",
"pkg-up": "^2.0.0", "pkg-up": "^2.0.0",
"read-pkg-up": "^3.0.0", "read-pkg-up": "^3.0.0",
"resolve": "^1.20.0", "resolve": "^1.20.0",
"tsconfig-paths": "^3.9.0" "tsconfig-paths": "^3.11.0"
}, },
"dependencies": { "dependencies": {
"debug": { "debug": {
@ -6158,33 +6296,6 @@
"esutils": "^2.0.2" "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": { "find-up": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
@ -6194,6 +6305,15 @@
"locate-path": "^2.0.0" "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": { "locate-path": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
@ -6292,9 +6412,9 @@
"dev": true "dev": true
}, },
"eslint-plugin-vue": { "eslint-plugin-vue": {
"version": "7.16.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.16.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.17.0.tgz",
"integrity": "sha512-0E2dVvVC7I2Xm1HXyx+ZwPj9CNX4NJjs4K4r+GVsHWyt5Pew3JLD4fI7A91b2jeL0TXE7LlszrwLSTJU9eqehw==", "integrity": "sha512-Rq5R2QetDCgC+kBFQw1+aJ5B93tQ4xqZvoCUxuIzwTonngNArsdP8ChM8PowIzsJvRtWl4ltGh/bZcN3xhFWSw==",
"dev": true, "dev": true,
"requires": { "requires": {
"eslint-utils": "^2.1.0", "eslint-utils": "^2.1.0",
@ -6786,9 +6906,9 @@
"dev": true "dev": true
}, },
"fastq": { "fastq": {
"version": "1.11.1", "version": "1.12.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.12.0.tgz",
"integrity": "sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==", "integrity": "sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==",
"dev": true, "dev": true,
"requires": { "requires": {
"reusify": "^1.0.4" "reusify": "^1.0.4"
@ -8179,6 +8299,24 @@
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true "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": { "is-extendable": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@ -8283,6 +8421,12 @@
"isobject": "^3.0.1" "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": { "is-regex": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz",
@ -8383,6 +8527,12 @@
"easy-stack": "^1.0.1" "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": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -8485,6 +8635,16 @@
"verror": "1.10.0" "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": { "killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@ -10647,6 +10807,15 @@
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true "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": { "promise-inflight": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "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": { "pump": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@ -10825,6 +11127,40 @@
"unpipe": "1.0.0" "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": { "read-pkg": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@ -12606,6 +12942,12 @@
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"dev": true "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": { "toposort": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz",
@ -12693,14 +13035,26 @@
"dev": true "dev": true
}, },
"tsconfig-paths": { "tsconfig-paths": {
"version": "3.10.1", "version": "3.11.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz",
"integrity": "sha512-rETidPDgCpltxF7MjBZlAFPUHv5aHH2MymyPvh+vEyWAED4Eb/WeMbsnD/JDr4OKPOA1TssDHgIcpTN5Kh0p6Q==", "integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==",
"dev": true, "dev": true,
"requires": { "requires": {
"json5": "^2.2.0", "@types/json5": "^0.0.29",
"json5": "^1.0.1",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"strip-bom": "^3.0.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": { "tslib": {
@ -13140,14 +13494,31 @@
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
"dev": true "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": { "vue": {
"version": "3.2.2", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.2.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.6.tgz",
"integrity": "sha512-D/LuzAV30CgNJYGyNheE/VUs5N4toL2IgmS6c9qeOxvyh0xyn4exyRqizpXIrsvfx34zG9x5gCI2tdRHCGvF9w==", "integrity": "sha512-Zlb3LMemQS3Xxa6xPsecu45bNjr1hxO8Bh5FUmE0Dr6Ot0znZBKiM47rK6O7FTcakxOnvVN+NTXWJF6u8ajpCQ==",
"requires": { "requires": {
"@vue/compiler-dom": "3.2.2", "@vue/compiler-dom": "3.2.6",
"@vue/runtime-dom": "3.2.2", "@vue/runtime-dom": "3.2.6",
"@vue/shared": "3.2.2" "@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": { "vue-demi": {
@ -13941,6 +14312,18 @@
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
"dev": true "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": { "word-wrap": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "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/core": "^2.0.0-alpha.24",
"@vuelidate/validators": "^2.0.0-alpha.21", "@vuelidate/validators": "^2.0.0-alpha.21",
"bootstrap": "^5.1.0", "bootstrap": "^5.1.0",
"core-js": "^3.16.1", "core-js": "^3.16.3",
"date-fns": "^2.23.0", "date-fns": "^2.23.0",
"date-fns-tz": "^1.1.6", "date-fns-tz": "^1.1.6",
"dompurify": "^2.3.1",
"marked": "^2.1.3", "marked": "^2.1.3",
"vue": "^3.2.2", "vue": "^3.2.6",
"vue-router": "^4.0.11", "vue-router": "^4.0.11",
"vuex": "^4.0.0-0" "vuex": "^4.0.0-0"
}, },
"devDependencies": { "devDependencies": {
"@types/bootstrap": "^5.1.1", "@types/bootstrap": "^5.1.2",
"@types/marked": "^2.0.4", "@types/dompurify": "^2.2.3",
"@typescript-eslint/eslint-plugin": "^4.29.1", "@types/marked": "^2.0.5",
"@typescript-eslint/parser": "^4.29.1", "@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0", "@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0", "@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0", "@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~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-standard": "^6.1.0",
"@vue/eslint-config-typescript": "^7.0.0", "@vue/eslint-config-typescript": "^7.0.0",
"eslint": "^7.32.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-node": "^11.1.0",
"eslint-plugin-promise": "^5.0.0", "eslint-plugin-promise": "^5.0.0",
"eslint-plugin-standard": "^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": "~1.37.0",
"sass-loader": "^10.0.0", "sass-loader": "^10.0.0",
"typescript": "~4.3.5", "typescript": "~4.3.5",

View File

@ -10,18 +10,18 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' 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 'bootstrap/dist/css/bootstrap.min.css' import "bootstrap/dist/css/bootstrap.min.css"
import '@mdi/font/css/materialdesignicons.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({ export default defineComponent({
name: 'App',
components: { components: {
AppFooter, AppFooter,
AppNav, AppNav,
@ -37,7 +37,7 @@ export default defineComponent({
* @returns "Yes" for true, "No" for false * @returns "Yes" for true, "No" for false
*/ */
export function yesOrNo (cond : boolean) : string { 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 * @returns The citizen's display name
*/ */
export function citizenName (cit : Citizen) : string { export function citizenName (cit : Citizen) : string {
return cit.realName || cit.displayName || cit.naUser return cit.realName ?? cit.displayName ?? cit.naUser
} }
</script> </script>

View File

@ -1,4 +1,3 @@
import { MarkedOptions } from 'marked'
import { import {
Citizen, Citizen,
Continent, Continent,
@ -18,7 +17,7 @@ import {
StoryEntry, StoryEntry,
StoryForm, StoryForm,
Success Success
} from './types' } from "./types"
/** /**
* Create a URL that will access the API * 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 // eslint-disable-next-line
const reqInit = (method : string, user : LogOnSuccess, body : any | undefined = undefined) : RequestInit => { const reqInit = (method : string, user : LogOnSuccess, body : any | undefined = undefined) : RequestInit => {
const headers = new Headers() const headers = new Headers()
headers.append('Authorization', `Bearer ${user.jwt}`) headers.append("Authorization", `Bearer ${user.jwt}`)
if (body) { if (body) {
headers.append('Content-Type', 'application/json') headers.append("Content-Type", "application/json")
return { return {
headers, headers,
method, method,
cache: 'no-cache', cache: "no-cache",
body: JSON.stringify(body) body: JSON.stringify(body)
} }
} }
@ -104,7 +103,7 @@ export default {
* @returns The user result, or an error * @returns The user result, or an error
*/ */
logOn: async (code : string) : Promise<LogOnSuccess | string> => { 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 if (resp.status === 200) return await resp.json() as LogOnSuccess
return `Error logging on - ${await resp.text()}` return `Error logging on - ${await resp.text()}`
}, },
@ -117,7 +116,7 @@ export default {
* @returns The citizen, or an error * @returns The citizen, or an error
*/ */
retrieve: async (id : string, user : LogOnSuccess) : Promise<Citizen | string | undefined> => 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 * Delete the current citizen's entire Jobs, Jobs, Jobs record
@ -126,7 +125,7 @@ export default {
* @returns Undefined if successful, an error if not * @returns Undefined if successful, an error if not
*/ */
delete: async (user : LogOnSuccess) : Promise<string | undefined> => 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 */ /** API functions for continents */
@ -138,7 +137,7 @@ export default {
* @returns All continents, or an error * @returns All continents, or an error
*/ */
all: async () : Promise<Continent[] | string | undefined> => 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 */ /** API functions for job listings */
@ -152,7 +151,7 @@ export default {
* @returns True if the addition was successful, an error string if not * @returns True if the addition was successful, an error string if not
*/ */
add: async (listing : ListingForm, user : LogOnSuccess) : Promise<boolean | string> => 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 * 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 * @returns The job listings the user has posted, or an error string
*/ */
mine: async (user : LogOnSuccess) : Promise<ListingForView[] | string | undefined> => mine: async (user : LogOnSuccess) : Promise<ListingForView[] | string | undefined> =>
apiResult<ListingForView[]>(await fetch(apiUrl('listings/mine'), reqInit('GET', user)), apiResult<ListingForView[]>(await fetch(apiUrl("listings/mine"), reqInit("GET", user)),
'retrieving your job listings'), "retrieving your job listings"),
/** /**
* Retrieve a job listing * Retrieve a job listing
@ -172,7 +171,7 @@ export default {
* @returns The job listing (if found), undefined (if not found), or an error string * @returns The job listing (if found), undefined (if not found), or an error string
*/ */
retreive: async (id : string, user : LogOnSuccess) : Promise<Listing | undefined | 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) * 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 * @returns The job listing (if found), undefined (if not found), or an error string
*/ */
retreiveForView: async (id : string, user : LogOnSuccess) : Promise<ListingForView | undefined | string> => retreiveForView: async (id : string, user : LogOnSuccess) : Promise<ListingForView | undefined | string> =>
apiResult<ListingForView>(await fetch(apiUrl(`listing/${id}/view`), reqInit('GET', user)), apiResult<ListingForView>(await fetch(apiUrl(`listing/${id}/view`), reqInit("GET", user)),
'retrieving job listing'), "retrieving job listing"),
/** /**
* Search for job listings using the given parameters * Search for job listings using the given parameters
@ -194,12 +193,12 @@ export default {
*/ */
search: async (query : ListingSearch, user : LogOnSuccess) : Promise<ListingForView[] | string | undefined> => { search: async (query : ListingSearch, user : LogOnSuccess) : Promise<ListingForView[] | string | undefined> => {
const params = new URLSearchParams() const params = new URLSearchParams()
if (query.continentId) params.append('continentId', query.continentId) if (query.continentId) params.append("continentId", query.continentId)
if (query.region) params.append('region', query.region) if (query.region) params.append("region", query.region)
params.append('remoteWork', query.remoteWork) params.append("remoteWork", query.remoteWork)
if (query.text) params.append('text', query.text) if (query.text) params.append("text", query.text)
return apiResult<ListingForView[]>(await fetch(apiUrl(`listing/search?${params.toString()}`), 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 * @returns True if the update was successful, an error string if not
*/ */
update: async (listing : ListingForm, user : LogOnSuccess) : Promise<boolean | string> => 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 */ /** API functions for profiles */
@ -223,7 +222,7 @@ export default {
* @returns True if the action was successful, or an error string if not * @returns True if the action was successful, or an error string if not
*/ */
markEmploymentFound: async (user : LogOnSuccess) : Promise<boolean | string> => { 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 if (result.ok) return true
return `${result.status} - ${result.statusText} (${await result.text()})` return `${result.status} - ${result.statusText} (${await result.text()})`
}, },
@ -236,13 +235,13 @@ export default {
*/ */
publicSearch: async (query : PublicSearch) : Promise<PublicSearchResult[] | string | undefined> => { publicSearch: async (query : PublicSearch) : Promise<PublicSearchResult[] | string | undefined> => {
const params = new URLSearchParams() const params = new URLSearchParams()
if (query.continentId) params.append('continentId', query.continentId) if (query.continentId) params.append("continentId", query.continentId)
if (query.region) params.append('region', query.region) if (query.region) params.append("region", query.region)
if (query.skill) params.append('skill', query.skill) if (query.skill) params.append("skill", query.skill)
params.append('remoteWork', query.remoteWork) params.append("remoteWork", query.remoteWork)
return apiResult<PublicSearchResult[]>( return apiResult<PublicSearchResult[]>(
await fetch(apiUrl(`profile/public-search?${params.toString()}`), { method: 'GET' }), await fetch(apiUrl(`profile/public-search?${params.toString()}`), { method: "GET" }),
'searching public profile data') "searching public profile data")
}, },
/** /**
@ -253,8 +252,8 @@ export default {
* @returns The profile (if found), undefined (if not found), or an error string * @returns The profile (if found), undefined (if not found), or an error string
*/ */
retreive: async (id : string | undefined, user : LogOnSuccess) : Promise<Profile | undefined | string> => { retreive: async (id : string | undefined, user : LogOnSuccess) : Promise<Profile | undefined | string> => {
const url = id ? `profile/${id}` : 'profile' const url = id ? `profile/${id}` : "profile"
const resp = await fetch(apiUrl(url), reqInit('GET', user)) const resp = await fetch(apiUrl(url), reqInit("GET", user))
if (resp.status === 200) return await resp.json() as Profile if (resp.status === 200) return await resp.json() as Profile
if (resp.status !== 204) return `Error retrieving profile - ${await resp.text()}` 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 * @returns The profile (if found), undefined (if not found), or an error string
*/ */
retreiveForView: async (id : string, user : LogOnSuccess) : Promise<ProfileForView | string | undefined> => 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 * Save a user's profile data
@ -277,7 +276,7 @@ export default {
* @returns True if the save was successful, an error string if not * @returns True if the save was successful, an error string if not
*/ */
save: async (data : ProfileForm, user : LogOnSuccess) : Promise<boolean | string> => 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 * Search for profiles using the given parameters
@ -288,12 +287,12 @@ export default {
*/ */
search: async (query : ProfileSearch, user : LogOnSuccess) : Promise<ProfileSearchResult[] | string | undefined> => { search: async (query : ProfileSearch, user : LogOnSuccess) : Promise<ProfileSearchResult[] | string | undefined> => {
const params = new URLSearchParams() const params = new URLSearchParams()
if (query.continentId) params.append('continentId', query.continentId) if (query.continentId) params.append("continentId", query.continentId)
if (query.skill) params.append('skill', query.skill) if (query.skill) params.append("skill", query.skill)
if (query.bioExperience) params.append('bioExperience', query.bioExperience) if (query.bioExperience) params.append("bioExperience", query.bioExperience)
params.append('remoteWork', query.remoteWork) params.append("remoteWork", query.remoteWork)
return apiResult<ProfileSearchResult[]>(await fetch(apiUrl(`profile/search?${params.toString()}`), 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 * @returns A count of profiles within the entire system
*/ */
count: async (user : LogOnSuccess) : Promise<number | string> => { 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) { if (resp.status === 200) {
const result = await resp.json() as Count const result = await resp.json() as Count
return result.count return result.count
@ -318,7 +317,7 @@ export default {
* @returns Undefined if successful, an error if not * @returns Undefined if successful, an error if not
*/ */
delete: async (user : LogOnSuccess) : Promise<string | undefined> => 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 */ /** 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 * @returns All success stories (if any exist), undefined (if none exist), or an error
*/ */
list: async (user : LogOnSuccess) : Promise<StoryEntry[] | string | undefined> => 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 * Retrieve a success story by its ID
@ -341,7 +340,7 @@ export default {
* @returns The success story, or an error * @returns The success story, or an error
*/ */
retrieve: async (id : string, user : LogOnSuccess) : Promise<Success | string | undefined> => 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 * Save a success story
@ -351,14 +350,8 @@ export default {
* @returns True if successful, an error string if not * @returns True if successful, an error string if not
*/ */
save: async (data : StoryForm, user : LogOnSuccess) : Promise<boolean | string> => 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 * from "./types"
export const markedOptions : MarkedOptions = {
gfm: true,
smartypants: true
}
export * from './types'

View File

@ -62,17 +62,17 @@ export interface Listing {
/** The data required to add or edit a job listing */ /** The data required to add or edit a job listing */
export class ListingForm { export class ListingForm {
/** The ID of the listing */ /** The ID of the listing */
id = '' id = ""
/** The listing title */ /** The listing title */
title = '' title = ""
/** The ID of the continent on which this opportunity exists */ /** The ID of the continent on which this opportunity exists */
continentId = '' continentId = ""
/** The region in which this opportunity exists */ /** The region in which this opportunity exists */
region = '' region = ""
/** Whether this is a remote work opportunity */ /** Whether this is a remote work opportunity */
remoteWork = false remoteWork = false
/** The text of the job listing */ /** The text of the job listing */
text = '' text = ""
/** The date by which this job listing is needed */ /** The date by which this job listing is needed */
neededBy : string | undefined neededBy : string | undefined
} }
@ -150,17 +150,17 @@ export class ProfileForm {
/** Whether this profile should appear in the public search */ /** Whether this profile should appear in the public search */
isPublic = false isPublic = false
/** The user's real name */ /** The user's real name */
realName = '' realName = ""
/** The ID of the continent on which the citizen is located */ /** The ID of the continent on which the citizen is located */
continentId = '' continentId = ""
/** The area within that continent where the citizen is located */ /** The area within that continent where the citizen is located */
region = '' region = ""
/** If the citizen is available for remote work */ /** If the citizen is available for remote work */
remoteWork = false remoteWork = false
/** If the citizen is seeking full-time employment */ /** If the citizen is seeking full-time employment */
fullTime = false fullTime = false
/** The user's professional biography */ /** The user's professional biography */
biography = '' biography = ""
/** The user's past experience */ /** The user's past experience */
experience : string | undefined experience : string | undefined
/** The skills for the user */ /** The skills for the user */
@ -248,11 +248,11 @@ export interface StoryEntry {
/** The data required to provide a success story */ /** The data required to provide a success story */
export class StoryForm { export class StoryForm {
/** The ID of this story */ /** The ID of this story */
id = '' id = ""
/** Whether the employment was obtained from Jobs, Jobs, Jobs */ /** Whether the employment was obtained from Jobs, Jobs, Jobs */
fromHere = false fromHere = false
/** The success story */ /** The success story */
story = '' story = ""
} }
/** A record of success finding employment */ /** A record of success finding employment */

View File

@ -1,34 +1,20 @@
<template lang="pug"> <template lang="pug">
span(@click='playFile') #[slot] #[audio(:id='clip'): source(:src='clipSource')] span(@click="playFile") #[slot] #[audio(:id="clip"): source(:src="clipSource")]
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' const props = defineProps<{
clip: string
}>()
export default defineComponent({ /** The full relative URL for the audio clip */
name: 'AudioClip', const clipSource = `/audio/${props.clip}.mp3`
props: {
clip: {
type: String,
required: true
}
},
setup (props) {
/** The full relative URL for the audio clip */
const clipSource = `/audio/${props.clip}.mp3`
/** Play the audio file */ /** Play the audio file */
const playFile = () => { const playFile = () => {
const audio = document.getElementById(props.clip) as HTMLAudioElement const audio = document.getElementById(props.clip) as HTMLAudioElement
audio.play() audio.play()
} }
return {
clipSource,
playFile
}
}
})
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View File

@ -1,32 +1,27 @@
<template lang="pug"> <template lang="pug">
.card: .card-body .card: .card-body
h6.card-title h6.card-title
a(href='#' :class="{ 'cp-c': collapsed, 'cp-o': !collapsed }" @click.prevent='toggle') {{headerText}} a(href="#" :class="{ 'cp-c': collapsed, 'cp-o': !collapsed }" @click.prevent="toggle") {{headerText}}
slot(v-if='!collapsed') slot(v-if="!collapsed")
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' interface Props {
headerText: string
collapsed: boolean
}
export default defineComponent({ const props = withDefaults(defineProps<Props>(), {
name: 'CollapsePanel', headerText: "Toggle",
emits: ['toggle'], collapsed: false
props: {
headerText: {
type: String,
default: 'Toggle'
},
collapsed: {
type: Boolean,
default: false
}
},
setup (props, { emit }) {
return {
toggle: () => emit('toggle', !props.collapsed)
}
}
}) })
const emit = defineEmits<{
(e: "toggle") : void
}>()
/** Emit the toggle event */
const toggle = () => emit("toggle", !props.collapsed)
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View File

@ -1,57 +1,58 @@
<template lang="pug"> <template lang="pug">
.form-floating .form-floating
select.form-select(id='continentId' :class="{ 'is-invalid': isInvalid}" :value='continentId' select.form-select(id="continentId" :class="{ 'is-invalid': isInvalid}" :value="continentId"
@change='continentChanged') @change="continentChanged")
option(value='') &ndash; {{emptyLabel}} &ndash; option(value="") &ndash; {{emptyLabel}} &ndash;
option(v-for='c in continents' :key='c.id' :value='c.id') {{c.name}} option(v-for="c in continents" :key="c.id" :value="c.id") {{c.name}}
label.jjj-required(for='continentId') Continent label.jjj-required(for="continentId") Continent
.invalid-feedback Please select a continent .invalid-feedback Please select a continent
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { useStore } from '@/store' import { useStore } from "@/store"
import { computed, defineComponent, onMounted, ref } from 'vue' import { computed, 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()
/** The continent ID, which this component can change */ interface Props {
const continentId = ref(props.modelValue) modelValue: string
topLabel?: string
isInvalid?: boolean
}
/** 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 * 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 * (This works around a really strange sequence where, if the "touch" call is directly wired up to the onChange event,
* event, the first time a value is selected, it doesn't stick (although the field is marked as touched). On second * the first time a value is selected, it doesn't stick (although the field is marked as touched). On second and
* and subsequent times, it worked. The solution here is to grab the value and update the reactive source for the * subsequent times, it worked. The solution here is to grab the value and update the reactive source for the form, then
* form, then manually set the field to touched; this restores the expected behavior. This is probably why the * manually set the field to touched; this restores the expected behavior. This is probably why the library doesn't hook
* library doesn't hook into the onChange event to begin with...) * into the onChange event to begin with...)
*/ */
const continentChanged = (e : Event) : boolean => { const continentChanged = (e : Event) : boolean => {
continentId.value = (e.target as HTMLSelectElement).value continentId.value = (e.target as HTMLSelectElement).value
emit('touch') emit("touch")
emit('update:modelValue', continentId.value) emit("update:modelValue", continentId.value)
return true return true
} }
onMounted(async () => await store.dispatch('ensureContinents')) onMounted(async () => await store.dispatch("ensureContinents"))
return { /** Accessor for the continent list */
continentId, const continents = computed(() => store.state.continents)
continents: computed(() => store.state.continents),
emptyLabel: props.topLabel || 'Select', /** The label to use for the top entry in the list */
continentChanged const emptyLabel = props.topLabel ?? "Select"
}
}
})
</script> </script>

View File

@ -1,22 +1,14 @@
<template lang="pug"> <template lang="pug">
template(v-if='errors.length > 0') template(v-if="errors.length > 0")
p The following error#[template(v-if='errors.length !== 1') s] occurred: p The following error#[template(v-if="errors.length !== 1") s] occurred:
ul: li(v-for='(error, idx) in errors' :key='idx') {{error}} ul: li(v-for="(error, idx) in errors" :key="idx") {{error}}
slot(v-else) slot(v-else)
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' const props = defineProps<{
errors: string[]
export default defineComponent({ }>()
name: 'ErrorList',
props: {
errors: {
type: Array,
required: true
}
}
})
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View File

@ -1,34 +1,15 @@
<template lang="pug"> <template lang="pug">
template(v-if='true') {{formatted}} template(v-if="true") {{formatted}}
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { format } from "date-fns"
import { format, parseJSON } from 'date-fns' import { parseToUtc } from "./"
import { utcToZonedTime } from 'date-fns-tz'
/** const props = defineProps<{
* Parse a date from its JSON representation to a UTC-aligned date date: string
* }>()
* @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)
}
export default defineComponent({ /** The formatted date */
name: 'FullDate', const formatted = format(parseToUtc(props.date), "PPP")
props: {
date: {
type: String,
required: true
}
},
setup (props) {
return {
formatted: format(parseToUtc(props.date), 'PPP')
}
}
})
</script> </script>

View File

@ -1,24 +1,15 @@
<template lang="pug"> <template lang="pug">
template(v-if='true') {{formatted}} template(v-if="true") {{formatted}}
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { format } from "date-fns"
import { format } from 'date-fns' import { parseToUtc } from "./"
import { parseToUtc } from './FullDate.vue'
export default defineComponent({ const props = defineProps<{
name: 'FullDateTime', date: string
props: { }>()
date: {
type: String, /** The formatted date/time */
required: true const formatted = format(parseToUtc(props.date), "PPPp")
}
},
setup (props) {
return {
formatted: format(parseToUtc(props.date), 'PPPp')
}
}
})
</script> </script>

View File

@ -1,22 +1,12 @@
<template lang="pug"> <template lang="pug">
span(:class='iconClass') span(:class="iconClass")
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' const props = defineProps<{
icon: string
}>()
export default defineComponent({ /** The CSS class to display the requested icon */
name: 'Icon', const iconClass = `mdi mdi-${props.icon}`
props: {
icon: {
type: String,
required: true
}
},
setup (props) {
return {
iconClass: `mdi mdi-${props.icon}`
}
}
})
</script> </script>

View File

@ -2,69 +2,62 @@
form.container form.container
.row .row
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3 .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 .col.col-xs-12.col-sm-6.col-lg-3
.form-floating .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)") @input="updateValue('region', $event.target.value)")
label(for='regionSearch') Region label(for="regionSearch") Region
.form-text (free-form text) .form-text (free-form text)
.col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0 .col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0
label.jjj-label Remote Work Opportunity? label.jjj-label Remote Work Opportunity?
br br
.form-check.form-check-inline .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', '')") @click="updateValue('remoteWork', '')")
label.form-check-label(for='remoteNull') No Selection label.form-check-label(for="remoteNull") No Selection
.form-check.form-check-inline .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')") @click="updateValue('remoteWork', 'yes')")
label.form-check-label(for='remoteYes') Yes label.form-check-label(for="remoteYes") Yes
.form-check.form-check-inline .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')") @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 .col.col-xs-12.col-sm-6.col-lg-3
.form-floating .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)") @input="updateValue('text', $event.target.value)")
label(for='textSearch') Job Listing Text label(for="textSearch") Job Listing Text
.form-text (free-form text) .form-text (free-form text)
.row: .col.col-xs-12 .row: .col.col-xs-12
br 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> </template>
<script lang="ts"> <script setup lang="ts">
import { ListingSearch } from '@/api' import { ListingSearch } from "@/api"
import { defineComponent, ref, Ref } from 'vue' import { ref } from "vue"
import ContinentList from './ContinentList.vue' import ContinentList from "./ContinentList.vue"
export default defineComponent({ const props = defineProps<{
name: 'ListingSearchForm', modelValue: ListingSearch
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 })
/** Emit a value update */ const emit = defineEmits<{
const updateValue = (key : string, value : string) => { (e: "search") : void
(e: "update:modelValue", value : ListingSearch) : void
}>()
/** 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 } criteria.value = { ...criteria.value, [key]: value }
emit('update:modelValue', criteria.value) emit("update:modelValue", criteria.value)
} }
return { /** Update the continent ID */
criteria, const updateContinent = (c : string) => updateValue("continentId", c)
updateContinent: (c : string) => updateValue('continentId', c),
updateValue
}
}
})
</script> </script>

View File

@ -1,47 +1,31 @@
<template lang="pug"> <template lang="pug">
div(v-if='loading') Loading&hellip; div(v-if="loading") Loading&hellip;
error-list(v-else :errors='errors') error-list(v-else :errors="errors")
slot slot
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, onMounted, ref } from 'vue' import { onMounted, ref } from "vue"
import ErrorList from './ErrorList.vue' import ErrorList from "./ErrorList.vue"
export default defineComponent({ const props = defineProps<{
name: 'LoadData', load: (errors : string[]) => Promise<unknown>
components: { ErrorList }, }>()
props: {
load: {
type: Function,
required: true
}
},
setup (props) {
/** Type the input function */
const func = props.load as (errors: string[]) => Promise<unknown>
/** Errors encountered during loading */ /** Errors encountered during loading */
const errors : string[] = [] const errors : string[] = []
/** Whether we are currently loading data */ /** Whether we are currently loading data */
const loading = ref(true) const loading = ref(true)
/** Call the data load function */ /** Call the data load function */
const loadData = async () => { const loadData = async () => {
try { try {
await func(errors) await props.load(errors)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
onMounted(loadData) onMounted(loadData)
return {
loading,
errors
}
}
})
</script> </script>

View File

@ -1,74 +1,60 @@
<template lang="pug"> <template lang="pug">
.col-12 .col-12
nav.nav.nav-pills.pb-1 nav.nav.nav-pills.pb-1
button(:class='sourceClass' @click.prevent='showMarkdown') Markdown button(:class="sourceClass" @click.prevent="showMarkdown") Markdown
| &nbsp; | &nbsp;
button(:class='previewClass' @click.prevent='showPreview') Preview button(:class="previewClass" @click.prevent="showPreview") Preview
section.preview(v-if='preview' v-html='previewHtml') section.preview(v-if="preview" v-html="previewHtml")
.form-floating(v-else) .form-floating(v-else)
textarea(:id='id' :class="{ 'form-control': true, 'md-edit': true, 'is-invalid': isInvalid }" rows='10' textarea.form-control.md-edit(:id="id" :class="{ 'is-invalid': isInvalid }" rows="10" v-text="text"
v-text='text' @input="$emit('update:text', $event.target.value)") @input="$emit('update:text', $event.target.value)")
.invalid-feedback Please enter some text for {{label}} .invalid-feedback Please enter some text for {{label}}
label(:for='id') {{label}} label(:for="id") {{label}}
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent, ref } from 'vue' import { computed, ref } from "vue"
import marked from 'marked' import { toHtml } from "@/markdown"
import { markedOptions } from '@/api'
export default defineComponent({ const props = defineProps<{
name: 'MarkdownEditor', id: string
props: { text: string
id: { label: string
type: String, isInvalid?: boolean
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)
/** The HTML rendered for preview purposes */ const emit = defineEmits<{
const previewHtml = ref('') (e: "update:text", value : string) : void
}>()
/** Show the Markdown source */ /** Whether to show the Markdown preview */
const showMarkdown = () => { const preview = ref(false)
/** The HTML rendered for preview purposes */
const previewHtml = ref("")
/** Show the Markdown source */
const showMarkdown = () => {
preview.value = false preview.value = false
} }
/** Show the Markdown preview */ /** Show the Markdown preview */
const showPreview = () => { const showPreview = () => {
previewHtml.value = marked(props.text, markedOptions) previewHtml.value = toHtml(props.text)
preview.value = true preview.value = true
} }
/** Button classes for the selected button */ /** Button classes for the selected button */
const selected = 'btn btn-primary btn-sm rounded-pill' const selected = "btn btn-primary btn-sm rounded-pill"
/** Button classes for the unselected button */ /** Button classes for the unselected button */
const unselected = 'btn btn-outline-secondary btn-sm rounded-pill' const unselected = "btn btn-outline-secondary btn-sm rounded-pill"
return { /** The CSS class for the Markdown source button */
preview, const sourceClass = computed(() => preview.value ? unselected : selected)
previewHtml,
showMarkdown, /** The CSS class for the Markdown preview button */
showPreview, const previewClass = computed(() => preview.value ? selected : unselected)
sourceClass: computed(() => preview.value ? unselected : selected),
previewClass: computed(() => preview.value ? selected : unselected)
}
}
})
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View File

@ -1,68 +1,58 @@
<template lang="pug"> <template lang="pug">
.modal.fade(id='maybeSaveModal' tabindex='-1' aria-labelledby='maybeSaveLabel' aria-hidden='true'): .modal-dialog: .modal-content .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-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-body You have modified the data on this page since it was last saved. What would you like to do?
.modal-footer .modal-footer
button.btn.btn-secondary(type='button' @click.prevent='onStay') Stay on This Page 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-primary(type="button" @click.prevent="onSave") Save Changes
button.btn.btn-danger(type='button' @click.prevent='onDiscard') Discard Changes button.btn.btn-danger(type="button" @click.prevent="onDiscard") Discard Changes
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent, onMounted, ref, Ref, watch } from 'vue' import { onMounted, ref, Ref, watch } from "vue"
import { RouteLocationNormalized, useRouter } from 'vue-router' import { RouteLocationNormalized, useRouter } from "vue-router"
import { Validation } from '@vuelidate/core' import { Validation } from "@vuelidate/core"
import { Modal } from 'bootstrap' import { Modal } from "bootstrap"
export default defineComponent({ const props = defineProps<{
name: 'MaybeSave', isShown: boolean
props: { toRoute: RouteLocationNormalized
isShown: { saveAction?: () => Promise<unknown>
type: Boolean, validator?: Validation
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()
/** The route where we tried to go */ const emit = defineEmits<{
const newRoute = computed(() => props.toRoute as RouteLocationNormalized) (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 router = useRouter()
const modal : Ref<Modal | undefined> = ref(undefined)
/** Save changes (if required) and go to the next route */ /** Reference to the modal dialog (we can't get it until the component is rendered) */
const onSave = async () => { const modal : Ref<Modal | undefined> = ref(undefined)
if (props.saveAction) await Promise.resolve(props.saveAction())
emit('close')
router.push(newRoute.value)
}
/** Discard changes (if required) and go to the next route */ /** Save changes (if required) and go to the next route */
const onDiscard = () => { const onSave = async () => {
if (props.validator) (props.validator as Validation).$reset() if (props.saveAction) await props.saveAction()
emit('close') emit("close")
router.push(newRoute.value) router.push(props.toRoute)
} }
onMounted(() => { /** Discard changes (if required) and go to the next route */
modal.value = new Modal(document.getElementById('maybeSaveModal') as HTMLElement, const onDiscard = () => {
{ backdrop: 'static', keyboard: false }) if (props.validator) props.validator.$reset()
}) emit("close")
router.push(props.toRoute)
}
/** Show or hide the modal based on the property value changing */ onMounted(() => {
watch(() => props.isShown, (toShow) => { modal.value = new Modal(document.getElementById("maybeSaveModal") as HTMLElement,
{ backdrop: "static", keyboard: false })
})
/** Show or hide the modal based on the property value changing */
watch(() => props.isShown, (toShow) => {
if (modal.value) { if (modal.value) {
if (toShow) { if (toShow) {
modal.value.show() modal.value.show()
@ -70,13 +60,8 @@ export default defineComponent({
modal.value.hide() modal.value.hide()
} }
} }
})
return {
onStay: () => emit('close'),
onSave,
onDiscard
}
}
}) })
/** Stay on this page with no changes; just close the modal */
const onStay = () => emit("close")
</script> </script>

View File

@ -1,39 +1,28 @@
<template lang="pug"> <template lang="pug">
p(v-if='false') p(v-if="false")
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, onMounted } from 'vue' import { onMounted, watch } from "vue"
export default defineComponent({ const props = defineProps<{
name: 'PageTitle', title: string
props: { }>()
title: {
type: String,
required: true
}
},
setup (props) {
/** The name of the application */
const appName = 'Jobs, Jobs, Jobs'
/** Set the page title based on the input title attribute */ /** The name of the application */
const setTitle = () => { const appName = "Jobs, Jobs, Jobs"
if (props.title === '') {
/** Set the page title based on the input title attribute */
const setTitle = () => {
if (props.title === "") {
document.title = appName document.title = appName
} else { } else {
document.title = `${props.title} | ${appName}` document.title = `${props.title} | ${appName}`
} }
} }
onMounted(setTitle) onMounted(setTitle)
return { /** Change the page title when the property changes */
setTitle watch(() => props.title, setTitle)
}
},
watch: {
title: 'setTitle'
}
})
</script> </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"> <template lang="pug">
footer: p.text-muted. footer: p.text-muted.
Jobs, Jobs, Jobs v{{appVersion}} &bull; #[router-link(to='/privacy-policy') Privacy Policy] Jobs, Jobs, Jobs v{{appVersion}} &bull; #[router-link(to="/privacy-policy") Privacy Policy]
&bull; #[router-link(to='/terms-of-service') Terms of Service] &bull; #[router-link(to="/terms-of-service") Terms of Service]
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { version } from "../../../package.json"
import { version } from '../../../package.json'
export default defineComponent({ let appVersion : string = version
name: 'AppFooter', while (appVersion.endsWith(".0")) {
setup () {
let appVersion : string = version
while (appVersion.endsWith('.0')) {
appVersion = appVersion.substring(0, appVersion.length - 2) appVersion = appVersion.substring(0, appVersion.length - 2)
} }
return {
appVersion
}
}
})
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View File

@ -1,40 +1,33 @@
<template lang="pug"> <template lang="pug">
aside.collapse.show.p-3 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; p &nbsp;
nav nav
template(v-if='isLoggedOn') template(v-if="isLoggedOn")
router-link(to='/citizen/dashboard') #[icon(icon='view-dashboard-variant')]&nbsp; Dashboard 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="/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="/profile/search") #[icon(icon="view-list-outline")]&nbsp; Employment Profiles
router-link(to='/success-story/list') #[icon(icon='thumb-up')]&nbsp; Success Stories router-link(to="/success-story/list") #[icon(icon="thumb-up")]&nbsp; Success Stories
.separator .separator
router-link(to='/listings/mine') #[icon(icon='sign-text')]&nbsp; My Job Listings 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="/citizen/profile") #[icon(icon="pencil")]&nbsp; My Employment Profile
.separator .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) template(v-else)
router-link(to='/') #[icon(icon='home')]&nbsp; Home router-link(to="/") #[icon(icon="home")]&nbsp; Home
router-link(to='/profile/seeking') #[icon(icon='view-list-outline')]&nbsp; Job Seekers 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="/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="/how-it-works") #[icon(icon="help-circle-outline")]&nbsp; How It Works
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent } from 'vue' import { computed } from "vue"
import { useStore } from '@/store' import { useStore } from "@/store"
export default defineComponent({ const store = useStore()
name: 'AppNav',
setup () {
const store = useStore()
return { /** Whether a user is logged in or not */
/** Whether a user is logged in or not */ const isLoggedOn = computed(() => store.state.user !== undefined)
isLoggedOn: computed(() => store.state.user !== undefined)
}
}
})
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View File

@ -1,50 +1,50 @@
<template lang="pug"> <template lang="pug">
div(aria-live='polite' aria-atomic='true' id='toastHost') div(aria-live="polite" aria-atomic="true" id="toastHost")
.toast-container.position-absolute.p-3.bottom-0.start-50.translate-middle-x(id='toasts') .toast-container.position-absolute.p-3.bottom-0.start-50.translate-middle-x(id="toasts")
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from "vue"
import { Toast } from 'bootstrap' import { Toast } from "bootstrap"
/** Remove a toast once it's hidden */ /** Remove a toast once it's hidden */
const removeToast = (event : Event) => (event.target as HTMLDivElement).remove() const removeToast = (event : Event) => (event.target as HTMLDivElement).remove()
/** Create a toast, add it to the DOM, and show it */ /** 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 let header : HTMLDivElement | undefined
if (level !== 'success') { if (level !== "success") {
// Create a heading, optionally including the process that generated the message // Create a heading, optionally including the process that generated the message
const heading = (typ : string) : string => { const heading = (typ : string) : string => {
const proc = process ? ` (${process})` : '' const proc = process ? ` (${process})` : ""
return `<span class="me-auto"><strong>${typ.toUpperCase()}</strong>${proc}</span>` return `<span class="me-auto"><strong>${typ.toUpperCase()}</strong>${proc}</span>`
} }
header = document.createElement('div') header = document.createElement("div")
header.className = 'toast-header' header.className = "toast-header"
header.innerHTML = heading(level === 'warning' ? level : 'error') header.innerHTML = heading(level === "warning" ? level : "error")
// Include a close button, as these will not auto-close // Include a close button, as these will not auto-close
const close = document.createElement('button') const close = document.createElement("button")
close.type = 'button' close.type = "button"
close.className = 'btn-close' close.className = "btn-close"
close.setAttribute('data-bs-dismiss', 'toast') close.setAttribute("data-bs-dismiss", "toast")
close.setAttribute('aria-label', 'Close') close.setAttribute("aria-label", "Close")
header.appendChild(close) header.appendChild(close)
} }
const body = document.createElement('div') const body = document.createElement("div")
body.className = 'toast-body' body.className = "toast-body"
body.innerHTML = message body.innerHTML = message
const toastEl = document.createElement('div') const toastEl = document.createElement("div")
toastEl.className = `toast bg-${level} text-white` toastEl.className = `toast bg-${level} text-white`
toastEl.setAttribute('role', 'alert') toastEl.setAttribute("role", "alert")
toastEl.setAttribute('aria-live', 'assertlive') toastEl.setAttribute("aria-live", "assertlive")
toastEl.setAttribute('aria-atomic', 'true') toastEl.setAttribute("aria-atomic", "true")
toastEl.addEventListener('hidden.bs.toast', removeToast) toastEl.addEventListener("hidden.bs.toast", removeToast)
if (header) toastEl.appendChild(header) if (header) toastEl.appendChild(header)
toastEl.appendChild(body) toastEl.appendChild(body)
;(document.getElementById('toasts') as HTMLDivElement).appendChild(toastEl) ;(document.getElementById("toasts") as HTMLDivElement).appendChild(toastEl)
new Toast(toastEl, { autohide: level === 'success' }).show() 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 * @param message The message to be displayed
*/ */
export function toastSuccess (message : string) : void { 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) * @param process The process which generated the warning (optional)
*/ */
export function toastWarning (message : string, process : string | undefined) : void { 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) * @param process The process which generated the error (optional)
*/ */
export function toastError (message : string, process : string | undefined) : void { export function toastError (message : string, process : string | undefined) : void {
createToast('danger', message, process) createToast("danger", message, process)
} }
export default defineComponent({ export default defineComponent({
name: 'AppToaster' name: "AppToaster"
}) })
</script> </script>

View File

@ -2,19 +2,11 @@
nav.navbar.navbar-light.bg-light nav.navbar.navbar-light.bg-light
span &nbsp; span &nbsp;
span.navbar-text. 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> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import AudioClip from "@/components/AudioClip.vue"
import AudioClip from '@/components/AudioClip.vue'
export default defineComponent({
name: 'TitleBar',
components: {
AudioClip
}
})
</script> </script>
<style lang="sass" scoped> <style lang="sass" scoped>

View File

@ -2,69 +2,62 @@
form.container form.container
.row .row
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3 .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 .col.col-xs-12.col-sm-6.col-md-4.col-lg-3
.form-floating .form-floating
input.form-control.form-control-sm(type='text' id='region' placeholder='(free-form text)' input.form-control.form-control-sm(type="text" id="region" placeholder="(free-form text)"
:value='criteria.region' @input="updateValue('region', $event.target.value)") :value="criteria.region" @input="updateValue('region', $event.target.value)")
label(for='region') Region label(for="region") Region
.form-text (free-form text) .form-text (free-form text)
.col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0 .col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0
label.jjj-label Seeking Remote Work? label.jjj-label Seeking Remote Work?
br br
.form-check.form-check-inline .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', '')") @click="updateValue('remoteWork', '')")
label.form-check-label(for='remoteNull') No Selection label.form-check-label(for="remoteNull") No Selection
.form-check.form-check-inline .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')") @click="updateValue('remoteWork', 'yes')")
label.form-check-label(for='remoteYes') Yes label.form-check-label(for="remoteYes") Yes
.form-check.form-check-inline .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')") @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 .col.col-xs-12.col-sm-6.col-lg-3
.form-floating .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)") :value="criteria.skill" @input="updateValue('skill', $event.target.value)")
label(for='skillSearch') Skill label(for="skillSearch") Skill
.form-text (free-form text) .form-text (free-form text)
.row: .col.col-xs-12 .row: .col.col-xs-12
br 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> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, ref, Ref } from 'vue' import { ref } from "vue"
import { PublicSearch } from '@/api' import { PublicSearch } from "@/api"
import ContinentList from '../ContinentList.vue' import ContinentList from "../ContinentList.vue"
export default defineComponent({ const props = defineProps<{
name: 'ProfilePublicSearchForm', modelValue: PublicSearch
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 })
/** Emit a value update */ const emit = defineEmits<{
const updateValue = (key : string, value : string) => { (e: "search") : void
(e: "update:modelValue", value : PublicSearch) : void
}>()
/** 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 } criteria.value = { ...criteria.value, [key]: value }
emit('update:modelValue', criteria.value) emit("update:modelValue", criteria.value)
} }
return { /** Update the continent ID */
criteria, const updateContinent = (c : string) => updateValue("continentId", c)
updateContinent: (c : string) => updateValue('continentId', c),
updateValue
}
}
})
</script> </script>

View File

@ -2,69 +2,62 @@
form.container form.container
.row .row
.col.col-xs-12.col-sm-6.col-md-4.col-lg-3 .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 .col.col-xs-12.col-sm-6.col-offset-md-2.col-lg-3.col-offset-lg-0
label.jjj-label Seeking Remote Work? label.jjj-label Seeking Remote Work?
br br
.form-check.form-check-inline .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', '')") @click="updateValue('remoteWork', '')")
label.form-check-label(for='remoteNull') No Selection label.form-check-label(for="remoteNull") No Selection
.form-check.form-check-inline .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')") @click="updateValue('remoteWork', 'yes')")
label.form-check-label(for='remoteYes') Yes label.form-check-label(for="remoteYes") Yes
.form-check.form-check-inline .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')") @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 .col.col-xs-12.col-sm-6.col-lg-3
.form-floating .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)") @input="updateValue('skill', $event.target.value)")
label(for='skillSearch') Skill label(for="skillSearch") Skill
.form-text (free-form text) .form-text (free-form text)
.col.col-xs-12.col-sm-6.col-lg-3 .col.col-xs-12.col-sm-6.col-lg-3
.form-floating .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)") @input="updateValue('bioExperience', $event.target.value)")
label(for='bioSearch') Bio / Experience label(for="bioSearch") Bio / Experience
.form-text (free-form text) .form-text (free-form text)
.row: .col.col-xs-12 .row: .col.col-xs-12
br 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> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, Ref, ref } from 'vue' import { ref } from "vue"
import { ProfileSearch } from '@/api' import { ProfileSearch } from "@/api"
import ContinentList from '../ContinentList.vue' import ContinentList from "../ContinentList.vue"
export default defineComponent({ const props = defineProps<{
name: 'ProfileSearchForm', modelValue: ProfileSearch
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 })
/** Emit a value update */ const emit = defineEmits<{
const updateValue = (key : string, value : string) => { (e: "search") : void
(e: "update:modelValue", value : ProfileSearch) : void
}>()
/** 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 } criteria.value = { ...criteria.value, [key]: value }
emit('update:modelValue', criteria.value) emit("update:modelValue", criteria.value)
} }
return { /** Update the continent ID */
criteria, const updateContinent = (c : string) => updateValue("continentId", c)
updateContinent: (c : string) => updateValue('continentId', c),
updateValue
}
}
})
</script> </script>

View File

@ -1,48 +1,44 @@
<template lang="pug"> <template lang="pug">
.row.pb-3 .row.pb-3
.col.col-xs-2.col-md-1.align-self-center .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 .col.col-xs-10.col-md-6
.form-floating .form-floating
input.form-control(type='text' :id='`skillDesc${skill.id}`' maxlength='100' input.form-control(type="text" :id="`skillDesc${skill.id}`" maxlength="100"
placeholder='A skill (language, design technique, process, etc.)' :value='skill.description' placeholder="A skill (language, design technique, process, etc.)" :value="skill.description"
@input="updateValue('description', $event.target.value)") @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.) .form-text A skill (language, design technique, process, etc.)
.col.col-xs-12.col-md-5 .col.col-xs-12.col-md-5
.form-floating .form-floating
input.form-control(type='text' :id='`skillNotes${skill.id}`' maxlength='100' input.form-control(type="text" :id="`skillNotes${skill.id}`" maxlength="100"
placeholder='A further description of the skill (100 characters max)' :value='skill.notes' placeholder="A further description of the skill (100 characters max)" :value="skill.notes"
@input="updateValue('notes', $event.target.value)") @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) .form-text A further description of the skill (100 characters max)
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, Ref, ref } from 'vue' import { Ref, ref } from "vue"
import { Skill } from '@/api' import { Skill } from "@/api"
export default defineComponent({ const props = defineProps<{
name: 'ProfileSkillEdit', modelValue: Skill
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 })
return { const emit = defineEmits<{
skill, (e: "input") : void
updateValue: (key : string, value : string) => { (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 } skill.value = { ...skill.value, [key]: value }
emit('update:modelValue', skill.value) emit("update:modelValue", skill.value)
emit('input') emit("input")
} }
}
}
})
</script> </script>

View File

@ -1,15 +1,15 @@
import { createApp } from 'vue' import { createApp } from "vue"
import App from './App.vue' import App from "./App.vue"
import router from './router' import router from "./router"
import store, { key } from './store' import store, { key } from "./store"
import Icon from './components/Icon.vue' import Icon from "./components/Icon.vue"
import PageTitle from './components/PageTitle.vue' import PageTitle from "./components/PageTitle.vue"
const app = createApp(App) const app = createApp(App)
.use(router) .use(router)
.use(store, key) .use(store, key)
app.component('Icon', Icon) app.component("Icon", Icon)
app.component('PageTitle', PageTitle) 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, RouteLocationNormalizedLoaded,
RouteRecordName, RouteRecordName,
RouteRecordRaw RouteRecordRaw
} from 'vue-router' } from "vue-router"
import store from '@/store' import store from "@/store"
import Home from '@/views/Home.vue' import Home from "@/views/Home.vue"
import LogOn from '@/views/citizen/LogOn.vue' import LogOn from "@/views/citizen/LogOn.vue"
/** The URL to which the user should be pointed once they have authorized with NAS */ /** 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 * Get a value from the query string
@ -27,120 +27,120 @@ export function queryValue (route: RouteLocationNormalizedLoaded, key : string)
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: '/', path: "/",
name: 'Home', name: "Home",
component: Home component: Home
}, },
{ {
path: '/how-it-works', path: "/how-it-works",
name: 'HowItWorks', name: "HowItWorks",
component: () => import(/* webpackChunkName: "help" */ '../views/HowItWorks.vue') component: () => import(/* webpackChunkName: "help" */ "../views/HowItWorks.vue")
}, },
{ {
path: '/privacy-policy', path: "/privacy-policy",
name: 'PrivacyPolicy', name: "PrivacyPolicy",
component: () => import(/* webpackChunkName: "legal" */ '../views/PrivacyPolicy.vue') component: () => import(/* webpackChunkName: "legal" */ "../views/PrivacyPolicy.vue")
}, },
{ {
path: '/terms-of-service', path: "/terms-of-service",
name: 'TermsOfService', name: "TermsOfService",
component: () => import(/* webpackChunkName: "legal" */ '../views/TermsOfService.vue') component: () => import(/* webpackChunkName: "legal" */ "../views/TermsOfService.vue")
}, },
// Citizen URLs // Citizen URLs
{ {
path: '/citizen/log-on', path: "/citizen/log-on",
name: 'LogOn', name: "LogOn",
component: LogOn component: LogOn
}, },
{ {
path: '/citizen/authorized', path: "/citizen/authorized",
name: 'CitizenAuthorized', name: "CitizenAuthorized",
component: () => import(/* webpackChunkName: "dashboard" */ '../views/citizen/Authorized.vue') component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Authorized.vue")
}, },
{ {
path: '/citizen/dashboard', path: "/citizen/dashboard",
name: 'Dashboard', name: "Dashboard",
component: () => import(/* webpackChunkName: "dashboard" */ '../views/citizen/Dashboard.vue') component: () => import(/* webpackChunkName: "dashboard" */ "../views/citizen/Dashboard.vue")
}, },
{ {
path: '/citizen/profile', path: "/citizen/profile",
name: 'EditProfile', name: "EditProfile",
component: () => import(/* webpackChunkName: "profedit" */ '../views/citizen/EditProfile.vue') component: () => import(/* webpackChunkName: "profedit" */ "../views/citizen/EditProfile.vue")
}, },
{ {
path: '/citizen/log-off', path: "/citizen/log-off",
name: 'LogOff', name: "LogOff",
component: () => import(/* webpackChunkName: "logoff" */ '../views/citizen/LogOff.vue') component: () => import(/* webpackChunkName: "logoff" */ "../views/citizen/LogOff.vue")
}, },
// Job Listing URLs // Job Listing URLs
{ {
path: '/help-wanted', path: "/help-wanted",
name: 'HelpWanted', name: "HelpWanted",
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/HelpWanted.vue') component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/HelpWanted.vue")
}, },
{ {
path: '/listing/:id/edit', path: "/listing/:id/edit",
name: 'EditListing', name: "EditListing",
component: () => import(/* webpackChunkName: "jobedit" */ '../views/listing/ListingEdit.vue') component: () => import(/* webpackChunkName: "jobedit" */ "../views/listing/ListingEdit.vue")
}, },
{ {
path: '/listing/:id/view', path: "/listing/:id/view",
name: 'ViewListing', name: "ViewListing",
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/ListingView.vue') component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/ListingView.vue")
}, },
{ {
path: '/listings/mine', path: "/listings/mine",
name: 'MyListings', name: "MyListings",
component: () => import(/* webpackChunkName: "joblist" */ '../views/listing/MyListings.vue') component: () => import(/* webpackChunkName: "joblist" */ "../views/listing/MyListings.vue")
}, },
// Profile URLs // Profile URLs
{ {
path: '/profile/:id/view', path: "/profile/:id/view",
name: 'ViewProfile', name: "ViewProfile",
component: () => import(/* webpackChunkName: "profview" */ '../views/profile/ProfileView.vue') component: () => import(/* webpackChunkName: "profview" */ "../views/profile/ProfileView.vue")
}, },
{ {
path: '/profile/search', path: "/profile/search",
name: 'SearchProfiles', name: "SearchProfiles",
component: () => import(/* webpackChunkName: "profview" */ '../views/profile/ProfileSearch.vue') component: () => import(/* webpackChunkName: "profview" */ "../views/profile/ProfileSearch.vue")
}, },
{ {
path: '/profile/seeking', path: "/profile/seeking",
name: 'PublicSearchProfiles', name: "PublicSearchProfiles",
component: () => import(/* webpackChunkName: "seeking" */ '../views/profile/Seeking.vue') component: () => import(/* webpackChunkName: "seeking" */ "../views/profile/Seeking.vue")
}, },
// "So Long" URLs // "So Long" URLs
{ {
path: '/so-long/options', path: "/so-long/options",
name: 'DeletionOptions', name: "DeletionOptions",
component: () => import(/* webpackChunkName: "so-long" */ '../views/so-long/DeletionOptions.vue') component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionOptions.vue")
}, },
{ {
path: '/so-long/success', path: "/so-long/success",
name: 'DeletionSuccess', name: "DeletionSuccess",
component: () => import(/* webpackChunkName: "so-long" */ '../views/so-long/DeletionSuccess.vue') component: () => import(/* webpackChunkName: "so-long" */ "../views/so-long/DeletionSuccess.vue")
}, },
// Success Story URLs // Success Story URLs
{ {
path: '/success-story/list', path: "/success-story/list",
name: 'ListStories', name: "ListStories",
component: () => import(/* webpackChunkName: "success" */ '../views/success-story/StoryList.vue') component: () => import(/* webpackChunkName: "success" */ "../views/success-story/StoryList.vue")
}, },
{ {
path: '/success-story/:id/edit', path: "/success-story/:id/edit",
name: 'EditStory', name: "EditStory",
component: () => import(/* webpackChunkName: "succedit" */ '../views/success-story/StoryEdit.vue') component: () => import(/* webpackChunkName: "succedit" */ "../views/success-story/StoryEdit.vue")
}, },
{ {
path: '/success-story/:id/view', path: "/success-story/:id/view",
name: 'ViewStory', name: "ViewStory",
component: () => import(/* webpackChunkName: "success" */ '../views/success-story/StoryView.vue') component: () => import(/* webpackChunkName: "success" */ "../views/success-story/StoryView.vue")
} }
] ]
/** The routes that do not require logins */ /** The routes that do not require logins */
const publicRoutes : Array<RouteRecordName> = [ const publicRoutes : Array<RouteRecordName> = [
'Home', 'HowItWorks', 'PrivacyPolicy', 'TermsOfService', 'LogOn', 'CitizenAuthorized', 'PublicSearchProfiles', "Home", "HowItWorks", "PrivacyPolicy", "TermsOfService", "LogOn", "CitizenAuthorized", "PublicSearchProfiles",
'DeletionSuccess' "DeletionSuccess"
] ]
const router = createRouter({ const router = createRouter({
@ -154,9 +154,9 @@ const router = createRouter({
// eslint-disable-next-line // eslint-disable-next-line
router.beforeEach((to : RouteLocationNormalized, from : RouteLocationNormalized) =>{ 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) 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 */ /* eslint-disable */
declare module '*.vue' { declare module "*.vue" {
import type { DefineComponent } from 'vue' import type { DefineComponent } from "vue"
const component: DefineComponent<{}, {}, any> const component: DefineComponent<{}, {}, any>
export default component 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 { InjectionKey } from "vue"
import { createStore, Store, useStore as baseUseStore } from 'vuex' import { createStore, Store, useStore as baseUseStore } from "vuex"
import api, { Continent, LogOnSuccess } from '../api' import api, { Continent, LogOnSuccess } from "../api"
/** The state tracked by the application */ /** The state tracked by the application */
export interface State { export interface State {
@ -13,7 +13,7 @@ export interface State {
} }
/** An injection key to identify this state with Vue */ /** 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 */ /** Use this store in component `setup` functions */
export function useStore () : Store<State> { export function useStore () : Store<State> {
@ -24,7 +24,7 @@ export default createStore({
state: () : State => { state: () : State => {
return { return {
user: undefined, 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: [] continents: []
} }
}, },
@ -45,19 +45,19 @@ export default createStore({
actions: { actions: {
async logOn ({ commit }, code: string) { async logOn ({ commit }, code: string) {
const logOnResult = await api.citizen.logOn(code) const logOnResult = await api.citizen.logOn(code)
if (typeof logOnResult === 'string') { if (typeof logOnResult === "string") {
commit('setLogOnState', logOnResult) commit("setLogOnState", logOnResult)
} else { } else {
commit('setUser', logOnResult) commit("setUser", logOnResult)
} }
}, },
async ensureContinents ({ state, commit }) { async ensureContinents ({ state, commit }) {
if (state.continents.length > 0) return if (state.continents.length > 0) return
const theSeven = await api.continent.all() const theSeven = await api.continent.all()
if (typeof theSeven === 'string') { if (typeof theSeven === "string") {
console.error(theSeven) console.error(theSeven)
} else { } else {
commit('setContinents', theSeven) commit("setContinents", theSeven)
} }
} }
}, },

View File

@ -1,6 +1,6 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Welcome!') page-title(title="Welcome!")
p &nbsp; p &nbsp;
p. p.
Welcome to Jobs, Jobs, Jobs (AKA No Agenda Careers), where citizens of Gitmo Nation can assist one another in 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. their work deconstructing the misinformation that passes for news on a day-to-day basis.
p. p.
Do you not understand the terms in the paragraph above? No worries; just head over to 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] #[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. #[em &nbsp;#[audio-clip(clip="thats-true") (that&rsquo;s true!)]] and find out what you&rsquo;re missing.
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import AudioClip from "@/components/AudioClip.vue"
import AudioClip from '@/components/AudioClip.vue'
export default defineComponent({
name: 'Home',
components: {
AudioClip
}
})
</script> </script>

View File

@ -1,6 +1,6 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='How It Works') page-title(title="How It Works")
h3 How It Works h3 How It Works
h4 Completing Your Profile h4 Completing Your Profile
ul ul
@ -12,9 +12,9 @@ article
li. li.
The &ldquo;Professional Biography&rdquo; and &ldquo;Experience&rdquo; sections support Markdown, a plain-text way 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 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 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 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. (turning "a quote" into &ldquo;a quote&rdquo;), tables, super/subscripts, and more.
li. li.
@ -63,7 +63,7 @@ article
h4 Help / Suggestions h4 Help / Suggestions
p. p.
This is open-source software 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" _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/issues" target="_blank") create an issue there], or look up
@danieljsummers on No Agenda Social. @danieljsummers on No Agenda Social.
</template> </template>

View File

@ -1,6 +1,6 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Privacy Policy') page-title(title="Privacy Policy")
h3 Privacy Policy h3 Privacy Policy
p: em (as of February 6#[sup th], 2021) 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. provide our content or whose products or services we think may interest you.
li. li.
Website: {{name}}&rsquo;s site, which can be accessed via this URL: 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. li You: a person or entity that is registered with {{name}} to use the Services.
h4 What Information Do We Collect? h4 What Information Do We Collect?
@ -332,18 +332,10 @@ article
h4 Contact Us h4 Contact Us
p Don't hesitate to contact us if you have any questions. 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> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' /** The name of the application */
const name = "Jobs, Jobs, Jobs"
export default defineComponent({
name: 'PrivacyPolicy',
setup () {
return {
name: 'Jobs, Jobs, Jobs'
}
}
})
</script> </script>

View File

@ -1,6 +1,6 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Terms of Service') page-title(title="Terms of Service")
h3 Terms of Service h3 Terms of Service
p: em (as of February 6#[sup th], 2021) p: em (as of February 6#[sup th], 2021)
@ -14,9 +14,9 @@ article
p. p.
Jobs, Jobs, Jobs is a service that allows individuals to enter and amend employment profiles, restricting access 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 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 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 h4 Liability
p. p.
@ -32,6 +32,6 @@ article
hr hr
p. 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. data.
</template> </template>

View File

@ -1,47 +1,41 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Logging on...') page-title(title="Logging on...")
p &nbsp; p &nbsp;
p(v-html='message') p(v-html="message")
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent, onMounted } from 'vue' import { computed, onMounted } from "vue"
import { useRouter } from 'vue-router' import { useRouter } from "vue-router"
import { useStore } from '@/store' import { useStore } from "@/store"
import { AFTER_LOG_ON_URL } from '@/router' import { AFTER_LOG_ON_URL } from "@/router"
export default defineComponent({ const router = useRouter()
name: 'Authorized', const store = useStore()
setup () {
const router = useRouter()
const store = useStore()
/** Pass the code to the API and exchange it for a user and a JWT */ /** Pass the code to the API and exchange it for a user and a JWT */
const logOn = async () => { const logOn = async () => {
const code = router.currentRoute.value.query.code const code = router.currentRoute.value.query.code
if (code) { if (code) {
await store.dispatch('logOn', code) await store.dispatch("logOn", code)
if (store.state.user !== undefined) { if (store.state.user !== undefined) {
const afterLogOnUrl = window.localStorage.getItem(AFTER_LOG_ON_URL) const afterLogOnUrl = window.localStorage.getItem(AFTER_LOG_ON_URL)
if (afterLogOnUrl) { if (afterLogOnUrl) {
window.localStorage.removeItem(AFTER_LOG_ON_URL) window.localStorage.removeItem(AFTER_LOG_ON_URL)
router.push(afterLogOnUrl) router.push(afterLogOnUrl)
} else { } else {
router.push('/citizen/dashboard') router.push("/citizen/dashboard")
} }
} }
} else { } else {
store.commit('setLogOnState', store.commit("setLogOnState",
'Did not receive a token from No Agenda Social (perhaps you clicked &ldquo;Cancel&rdquo;?)') "Did not receive a token from No Agenda Social (perhaps you clicked &ldquo;Cancel&rdquo;?)")
}
} }
}
onMounted(logOn) onMounted(logOn)
return { /** Accessor for the log on state */
message: computed(() => store.state.logOnState) const message = computed(() => store.state.logOnState)
}
}
})
</script> </script>

View File

@ -1,93 +1,77 @@
<template lang="pug"> <template lang="pug">
article.container article.container
page-title(title='Dashboard') page-title(title="Dashboard")
h3.pb-4 Welcome, {{user.name}} 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 .col: .card.h-100
h5.card-header Your Profile h5.card-header Your Profile
.card-body .card-body
h6.card-subtitle.mb-3.text-muted.fst-italic Last updated #[full-date-time(:date='profile.lastUpdatedOn')] h6.card-subtitle.mb-3.text-muted.fst-italic Last updated #[full-date-time(:date="profile.lastUpdatedOn")]
p.card-text(v-if='profile') p.card-text(v-if="profile")
| Your profile currently lists {{profile.skills.length}} | Your profile currently lists {{profile.skills.length}}
| skill#[template(v-if='profile.skills.length !== 1') s]. | skill#[template(v-if="profile.skills.length !== 1") s].
span(v-if='profile.seekingEmployment') span(v-if="profile.seekingEmployment")
br br
br br
| Your profile indicates that you are seeking employment. Once you find it, | 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). p.card-text(v-else).
You do not have an employment profile established; click below (or &ldquo;Edit Profile&rdquo; in the menu) to You do not have an employment profile established; click below (or &ldquo;Edit Profile&rdquo; in the menu) to
get started! get started!
.card-footer .card-footer
template(v-if='profile') template(v-if="profile")
router-link.btn.btn-outline-secondary(:to='`/profile/${user.citizenId}/view`') View Profile router-link.btn.btn-outline-secondary(:to="`/profile/${user.citizenId}/view`") View Profile
| &nbsp; &nbsp; | &nbsp; &nbsp;
router-link.btn.btn-outline-secondary(to='/citizen/profile') Edit Profile router-link.btn.btn-outline-secondary(to="/citizen/profile") Edit Profile
router-link.btn.btn-primary(v-else to='/citizen/profile') Create Profile router-link.btn.btn-primary(v-else to="/citizen/profile") Create Profile
.col: .card.h-100 .col: .card.h-100
h5.card-header Other Citizens h5.card-header Other Citizens
.card-body .card-body
h6.card-subtitle.mb-3.text-muted.fst-italic 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 template(v-else) {{profileCount}} Total
| Employment Profile#[template(v-if='profileCount !== 1') s] | 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-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-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; 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 &nbsp;
p. p.
To see how this application works, check out &ldquo;How It Works&rdquo; in the sidebar (last updated June To see how this application works, check out &ldquo;How It Works&rdquo; in the sidebar (last updated June
14#[sup th], 2021). 14#[sup th], 2021).
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, Ref, ref } from 'vue' import { Ref, ref } from "vue"
import api, { LogOnSuccess, Profile } from '@/api' import api, { LogOnSuccess, Profile } from "@/api"
import { useStore } from '@/store' import { useStore } from "@/store"
import FullDateTime from '@/components/FullDateTime.vue' import FullDateTime from "@/components/FullDateTime.vue"
import LoadData from '@/components/LoadData.vue' import LoadData from "@/components/LoadData.vue"
export default defineComponent({ const store = useStore()
name: 'Dashboard',
components: {
FullDateTime,
LoadData
},
setup () {
const store = useStore()
/** The currently logged-in user */ /** The currently logged-in user */
const user = store.state.user as LogOnSuccess const user = store.state.user as LogOnSuccess
/** The user's profile */ /** The user's profile */
const profile : Ref<Profile | undefined> = ref(undefined) const profile : Ref<Profile | undefined> = ref(undefined)
/** A count of profiles in the system */ /** A count of profiles in the system */
const profileCount = ref(0) const profileCount = ref(0)
const retrieveData = async (errors : string[]) => { const retrieveData = async (errors : string[]) => {
const profileResult = await api.profile.retreive(undefined, user) const profileResult = await api.profile.retreive(undefined, user)
if (typeof profileResult === 'string') { if (typeof profileResult === "string") {
errors.push(profileResult) errors.push(profileResult)
} else if (typeof profileResult !== 'undefined') { } else if (typeof profileResult !== "undefined") {
profile.value = profileResult profile.value = profileResult
} }
const count = await api.profile.count(user) const count = await api.profile.count(user)
if (typeof count === 'string') { if (typeof count === "string") {
errors.push(count) errors.push(count)
} else { } else {
profileCount.value = count profileCount.value = count
} }
} }
return {
retrieveData,
user,
profile,
profileCount
}
}
})
</script> </script>

View File

@ -1,47 +1,47 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Edit Profile') page-title(title="Edit Profile")
h3.pb-3 My Employment 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 .col-12.col-sm-10.col-md-8.col-lg-6
.form-floating .form-floating
input.form-control(type='text' id='realName' v-model='v$.realName.$model' maxlength='255' input.form-control(type="text" id="realName" v-model="v$.realName.$model" maxlength="255"
placeholder='Leave blank to use your NAS display name') placeholder="Leave blank to use your NAS display name")
label(for='realName') Real Name label(for="realName") Real Name
.form-text Leave blank to use your NAS display name .form-text Leave blank to use your NAS display name
.col-12 .col-12
.form-check .form-check
input.form-check-input(type='checkbox' id='isSeeking' v-model='v$.isSeekingEmployment.$model') input.form-check-input(type="checkbox" id="isSeeking" v-model="v$.isSeekingEmployment.$model")
label.form-check-label(for='isSeeking') I am currently seeking employment label.form-check-label(for="isSeeking") I am currently seeking employment
p(v-if='profile.isSeekingEmployment'): em. p(v-if="profile.isSeekingEmployment"): em.
If you have found employment, consider 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 .col-12.col-sm-6.col-md-4
continent-list(v-model='v$.continentId.$model' :isInvalid='v$.continentId.$error' continent-list(v-model="v$.continentId.$model" :isInvalid="v$.continentId.$error"
@touch='v$.continentId.$touch() || true') @touch="v$.continentId.$touch() || true")
.col-12.col-sm-6.col-md-8 .col-12.col-sm-6.col-md-8
.form-floating .form-floating
input.form-control(type='text' id='region' :class="{ 'is-invalid': v$.region.$error }" input.form-control(type="text" id="region" :class="{ 'is-invalid': v$.region.$error }"
v-model='v$.region.$model' maxlength='255' v-model="v$.region.$model" maxlength="255"
placeholder='Country, state, geographic area, etc.') placeholder="Country, state, geographic area, etc.")
#regionFeedback.invalid-feedback Please enter a region #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. .form-text Country, state, geographic area, etc.
markdown-editor(id='bio' label='Professional Biography' v-model:text='v$.biography.$model' markdown-editor(id="bio" label="Professional Biography" v-model:text="v$.biography.$model"
:isInvalid='v$.biography.$error') :isInvalid="v$.biography.$error")
.col-12.col-offset-md-2.col-md-4 .col-12.col-offset-md-2.col-md-4
.form-check .form-check
input.form-check-input(type='checkbox' id='isRemote' v-model='v$.remoteWork.$model') 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 label.form-check-label(for="isRemote") I am looking for remote work
.col-12.col-md-4 .col-12.col-md-4
.form-check .form-check
input.form-check-input(type='checkbox' id='isFullTime' v-model='v$.fullTime.$model') 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 label.form-check-label(for="isFullTime") I am looking for full-time work
.col-12 .col-12
hr hr
h4.pb-2 Skills &nbsp;#[button.btn.btn-sm.btn-outline-primary.rounded-pill(@click.prevent='addSkill') Add a Skill] 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]' 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') @remove="removeSkill(skill.id)" @input="v$.skills.$touch")
.col-12 .col-12
hr hr
h4 Experience h4 Experience
@ -49,79 +49,69 @@ article
This application does not have a place to individually list your chronological job history; however, you can use 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 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. 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 .col-12: .form-check
input.form-check-input(type='checkbox' id='isPublic' v-model='v$.isPublic.$model') 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) label.form-check-label(for="isPublic") Allow my profile to be searched publicly (outside NA Social)
.col-12 .col-12
p.text-danger(v-if='v$.$error') Please correct the errors above 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 button.btn.btn-primary(@click.prevent="saveProfile") #[icon(icon="content-save-outline")]&nbsp; Save
template(v-if='!isNew') template(v-if="!isNew")
| &nbsp; &nbsp; | &nbsp; &nbsp;
router-link.btn.btn-outline-secondary(:to='`/profile/${user.citizenId}/view`'). router-link.btn.btn-outline-secondary(:to="`/profile/${user.citizenId}/view`").
#[icon(icon='file-account-outline')]&nbsp; View Your User Profile #[icon(icon="file-account-outline")]&nbsp; View Your User Profile
hr hr
p.text-muted.fst-italic. p.text-muted.fst-italic.
(If you want to delete your profile, or your entire account, (If you want to delete your profile, or your entire account,
#[router-link(to='/so-long/options') see your deletion options here].) #[router-link(to="/so-long/options") see your deletion options here].)
maybe-save(:isShown='confirmNavShown' :toRoute='nextRoute' :saveAction='saveProfile' :validator='v$' maybe-save(:isShown="confirmNavShown" :toRoute="nextRoute" :saveAction="saveProfile" :validator="v$"
@close='confirmClose') @close="confirmClose")
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent, ref, reactive, Ref } from 'vue' import { computed, ref, reactive, Ref } from "vue"
import { onBeforeRouteLeave, RouteLocationNormalized } from 'vue-router' import { onBeforeRouteLeave, RouteLocationNormalized } from "vue-router"
import useVuelidate from '@vuelidate/core' import useVuelidate from "@vuelidate/core"
import { required } from '@vuelidate/validators' import { required } from "@vuelidate/validators"
import api, { Citizen, LogOnSuccess, Profile, ProfileForm } from '@/api' import api, { Citizen, LogOnSuccess, Profile, ProfileForm } from "@/api"
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue' import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
import { useStore } from '@/store' import { useStore } from "@/store"
import ContinentList from '@/components/ContinentList.vue' import ContinentList from "@/components/ContinentList.vue"
import LoadData from '@/components/LoadData.vue' import LoadData from "@/components/LoadData.vue"
import MarkdownEditor from '@/components/MarkdownEditor.vue' import MarkdownEditor from "@/components/MarkdownEditor.vue"
import MaybeSave from '@/components/MaybeSave.vue' import MaybeSave from "@/components/MaybeSave.vue"
import ProfileSkillEdit from '@/components/profile/SkillEdit.vue' import ProfileSkillEdit from "@/components/profile/SkillEdit.vue"
export default defineComponent({ const store = useStore()
name: 'EditProfile',
components: {
ContinentList,
LoadData,
MarkdownEditor,
MaybeSave,
ProfileSkillEdit
},
setup () {
const store = useStore()
/** The currently logged-on user */ /** The currently logged-on user */
const user = store.state.user as LogOnSuccess const user = store.state.user as LogOnSuccess
/** Whether this is a new profile */ /** Whether this is a new profile */
const isNew = ref(false) const isNew = ref(false)
/** The starting values for a new employment profile */ /** The starting values for a new employment profile */
const newProfile : Profile = { const newProfile : Profile = {
id: user.citizenId, id: user.citizenId,
seekingEmployment: false, seekingEmployment: false,
isPublic: false, isPublic: false,
continentId: '', continentId: "",
region: '', region: "",
remoteWork: false, remoteWork: false,
fullTime: false, fullTime: false,
biography: '', biography: "",
lastUpdatedOn: '', lastUpdatedOn: "",
experience: undefined, experience: undefined,
skills: [] skills: []
} }
/** The user's current profile (plus a few items, adapted for editing) */ /** The user's current profile (plus a few items, adapted for editing) */
const profile = reactive(new ProfileForm()) const profile = reactive(new ProfileForm())
/** The validation rules for the form */ /** The validation rules for the form */
const rules = computed(() => ({ const rules = computed(() => ({
realName: { }, realName: { },
isSeekingEmployment: { }, isSeekingEmployment: { },
isPublic: { }, isPublic: { },
@ -132,21 +122,21 @@ export default defineComponent({
biography: { required }, biography: { required },
experience: { }, experience: { },
skills: { } skills: { }
})) }))
/** Initialize form validation */ /** Initialize form validation */
const v$ = useVuelidate(rules, profile, { $lazy: true }) const v$ = useVuelidate(rules, profile, { $lazy: true })
/** Retrieve the user's profile and their real name */ /** Retrieve the user's profile and their real name */
const retrieveData = async (errors : string[]) => { const retrieveData = async (errors : string[]) => {
const profileResult = await api.profile.retreive(undefined, user) const profileResult = await api.profile.retreive(undefined, user)
if (typeof profileResult === 'string') { if (typeof profileResult === "string") {
errors.push(profileResult) errors.push(profileResult)
} else if (typeof profileResult === 'undefined') { } else if (typeof profileResult === "undefined") {
isNew.value = true isNew.value = true
} }
const nameResult = await api.citizen.retrieve(user.citizenId, user) const nameResult = await api.citizen.retrieve(user.citizenId, user)
if (typeof nameResult === 'string') { if (typeof nameResult === "string") {
errors.push(nameResult) errors.push(nameResult)
} }
if (errors.length > 0) return if (errors.length > 0) return
@ -161,66 +151,53 @@ export default defineComponent({
profile.biography = p.biography profile.biography = p.biography
profile.experience = p.experience profile.experience = p.experience
profile.skills = p.skills profile.skills = p.skills
profile.realName = typeof nameResult !== 'undefined' ? (nameResult as Citizen).realName || '' : '' profile.realName = typeof nameResult !== "undefined" ? (nameResult as Citizen).realName ?? "" : ""
} }
/** The ID for new skills */ /** The ID for new skills */
let newSkillId = 0 let newSkillId = 0
/** Add a skill to the profile */ /** Add a skill to the profile */
const addSkill = () => { const addSkill = () => {
profile.skills.push({ id: `new${newSkillId++}`, description: '', notes: undefined }) profile.skills.push({ id: `new${newSkillId++}`, description: "", notes: undefined })
v$.value.skills.$touch() v$.value.skills.$touch()
} }
/** Remove the given skill from the profile */ /** Remove the given skill from the profile */
const removeSkill = (skillId : string) => { const removeSkill = (skillId : string) => {
profile.skills = profile.skills.filter(s => s.id !== skillId) profile.skills = profile.skills.filter(s => s.id !== skillId)
v$.value.skills.$touch() v$.value.skills.$touch()
} }
/** Save the current profile values */ /** Save the current profile values */
const saveProfile = async () => { const saveProfile = async () => {
v$.value.$touch() v$.value.$touch()
if (v$.value.$error) return if (v$.value.$error) return
// Remove any blank skills before submitting // Remove any blank skills before submitting
profile.skills = profile.skills.filter(s => !(s.description.trim() === '' && (s.notes || '').trim() === '')) profile.skills = profile.skills.filter(s => !(s.description.trim() === "" && (s.notes ?? "").trim() === ""))
const saveResult = await api.profile.save(profile, user) const saveResult = await api.profile.save(profile, user)
if (typeof saveResult === 'string') { if (typeof saveResult === "string") {
toastError(saveResult, 'saving profile') toastError(saveResult, "saving profile")
} else { } else {
toastSuccess('Profile Saved Successfuly') toastSuccess("Profile Saved Successfuly")
v$.value.$reset() v$.value.$reset()
} }
} }
/** Whether the navigation confirmation is shown */ /** Whether the navigation confirmation is shown */
const confirmNavShown = ref(false) const confirmNavShown = ref(false)
/** The "next" route (will be navigated or cleared) */ /** The "next" route (will be navigated or cleared) */
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined) const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
/** If the user has unsaved changes, give them an opportunity to save before moving on */ /** If the user has unsaved changes, give them an opportunity to save before moving on */
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
if (!v$.value.$anyDirty) return true if (!v$.value.$anyDirty) return true
nextRoute.value = to nextRoute.value = to
confirmNavShown.value = true confirmNavShown.value = true
return false return false
})
return {
v$,
retrieveData,
user,
isNew,
profile,
addSkill,
removeSkill,
saveProfile,
confirmNavShown,
nextRoute,
confirmClose: () => { confirmNavShown.value = false }
}
}
}) })
/** Close the navigation confirmation modal */
const confirmClose = () => { confirmNavShown.value = false }
</script> </script>

View File

@ -1,29 +1,22 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Logging off...') page-title(title="Logging off...")
p &nbsp; p &nbsp;
p.fst-italic Logging off&hellip; p.fst-italic Logging off&hellip;
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, onMounted } from 'vue' import { onMounted } from "vue"
import { useRouter } from 'vue-router' import { useRouter } from "vue-router"
import { toastSuccess } from '@/components/layout/AppToaster.vue' import { toastSuccess } from "@/components/layout/AppToaster.vue"
import { useStore } from '@/store' import { useStore } from "@/store"
export default defineComponent({ const store = useStore()
name: 'LogOff', const router = useRouter()
setup () {
const store = useStore()
const router = useRouter()
onMounted(() => { onMounted(() => {
store.commit('clearUser') store.commit("clearUser")
toastSuccess('Log Off Successful &nbsp; | &nbsp; <strong>Have a Nice Day!</strong>') toastSuccess("Log Off Successful &nbsp; | &nbsp; <strong>Have a Nice Day!</strong>")
router.push('/') router.push("/")
})
return { }
}
}) })
</script> </script>

View File

@ -4,18 +4,14 @@ article
p.fst-italic Sending you over to No Agenda Social to log on; see you back in just a second&hellip; p.fst-italic Sending you over to No Agenda Social to log on; see you back in just a second&hellip;
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'
/** /**
* This component simply redirects the user to the No Agenda Social authorization page; it is separate here so that it * 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. * can be called from two different places, and allow the app to support direct links to authorized content.
*/ */
export default defineComponent({
name: 'LogOn', /** The authorization URL to which the user should be directed */
setup () { const authUrl = (() => {
/** The authorization URL to which the user should be directed */
const authUrl = (() => {
/** The client ID for Jobs, Jobs, Jobs at No Agenda Social */ /** The client ID for Jobs, Jobs, Jobs at No Agenda Social */
const id = 'k_06zlMy0N451meL4AqlwMQzs5PYr6g3d2Q_dCT-OjU' const id = 'k_06zlMy0N451meL4AqlwMQzs5PYr6g3d2Q_dCT-OjU'
const client = `client_id=${id}` const client = `client_id=${id}`
@ -23,10 +19,6 @@ export default defineComponent({
const redirect = `redirect_uri=${document.location.origin}/citizen/authorized` const redirect = `redirect_uri=${document.location.origin}/citizen/authorized`
const respType = 'response_type=code' const respType = 'response_type=code'
return `https://noagendasocial.com/oauth/authorize?${client}&${scope}&${redirect}&${respType}` return `https://noagendasocial.com/oauth/authorize?${client}&${scope}&${redirect}&${respType}`
})() })()
document.location.assign(authUrl) document.location.assign(authUrl)
return { }
}
})
</script> </script>

View File

@ -1,104 +1,97 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Help Wanted') page-title(title="Help Wanted")
h3.pb-3 Help Wanted h3.pb-3 Help Wanted
p(v-if="!searched"). p(v-if="!searched").
Enter relevant criteria to find results, or just click &ldquo;Search&rdquo; to see all current job listings. 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') collapse-panel(headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse")
listing-search-form(v-model='criteria' @search='doSearch') listing-search-form(v-model="criteria" @search="doSearch")
error-list(:errors='errors') error-list(:errors="errors")
p.pt-3(v-if='searching') Searching job listings&hellip; p.pt-3(v-if="searching") Searching job listings&hellip;
template(v-else) 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 thead: tr
th(scope='col') Listing th(scope="col") Listing
th(scope='col') Title th(scope="col") Title
th(scope='col') Location th(scope="col") Location
th.text-center(scope='col') Remote? th.text-center(scope="col") Remote?
th.text-center(scope='col') Needed By th.text-center(scope="col") Needed By
tbody: tr(v-for='it in results' :key='it.listing.id') tbody: tr(v-for="it in results" :key="it.listing.id")
td: router-link(:to='`/listing/${it.listing.id}/view`') View td: router-link(:to="`/listing/${it.listing.id}/view`") View
td {{it.listing.title}} td {{it.listing.title}}
td {{it.continent.name}} / {{it.listing.region}} td {{it.continent.name}} / {{it.listing.region}}
td.text-center {{yesOrNo(it.listing.remoteWork)}} 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 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> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, ref, Ref, watch } from 'vue' import { ref, Ref, watch } from "vue"
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from "vue-router"
import { formatNeededBy } from './ListingView.vue' import { formatNeededBy } from "./"
import { yesOrNo } from '@/App.vue' import { yesOrNo } from "@/App.vue"
import api, { ListingForView, ListingSearch, LogOnSuccess } from '@/api' import api, { ListingForView, ListingSearch, LogOnSuccess } from "@/api"
import { queryValue } from '@/router' import { queryValue } from "@/router"
import { useStore } from '@/store' import { useStore } from "@/store"
import CollapsePanel from '@/components/CollapsePanel.vue' import CollapsePanel from "@/components/CollapsePanel.vue"
import ErrorList from '@/components/ErrorList.vue' import ErrorList from "@/components/ErrorList.vue"
import ListingSearchForm from '@/components/ListingSearchForm.vue' import ListingSearchForm from "@/components/ListingSearchForm.vue"
export default defineComponent({ const store = useStore()
components: { const route = useRoute()
CollapsePanel, const router = useRouter()
ErrorList,
ListingSearchForm
},
setup () {
const store = useStore()
const route = useRoute()
const router = useRouter()
/** Any errors encountered while retrieving data */ /** Any errors encountered while retrieving data */
const errors : Ref<string[]> = ref([]) const errors : Ref<string[]> = ref([])
/** Whether we are currently searching (retrieving data) */ /** Whether we are currently searching (retrieving data) */
const searching = ref(false) const searching = ref(false)
/** Whether a search has been performed on this page since it has been loaded */ /** Whether a search has been performed on this page since it has been loaded */
const searched = ref(false) const searched = ref(false)
/** An empty set of search criteria */ /** An empty set of search criteria */
const emptyCriteria = { const emptyCriteria = {
continentId: '', continentId: "",
region: undefined, region: undefined,
remoteWork: '', remoteWork: "",
text: undefined text: undefined
} }
/** The search criteria being built from the page */ /** The search criteria being built from the page */
const criteria : Ref<ListingSearch> = ref(emptyCriteria) const criteria : Ref<ListingSearch> = ref(emptyCriteria)
/** The current search results */ /** The current search results */
const results : Ref<ListingForView[]> = ref([]) const results : Ref<ListingForView[]> = ref([])
/** Whether the search criteria should be collapsed */ /** Whether the search criteria should be collapsed */
const isCollapsed = ref(searched.value && results.value.length > 0) const isCollapsed = ref(searched.value && results.value.length > 0)
/** Set up the page to match its requested state */ /** Set up the page to match its requested state */
const setUpPage = async () => { const setUpPage = async () => {
if (queryValue(route, 'searched') === 'true') { if (queryValue(route, "searched") === "true") {
searched.value = true searched.value = true
try { try {
searching.value = true searching.value = true
// Hold variable for ensuring continent ID is not undefined here, but excluded from search payload // Hold variable for ensuring continent ID is not undefined here, but excluded from search payload
const contId = queryValue(route, 'continentId') const contId = queryValue(route, "continentId")
const searchParams : ListingSearch = { const searchParams : ListingSearch = {
continentId: contId === '' ? undefined : contId, continentId: contId === "" ? undefined : contId,
region: queryValue(route, 'region'), region: queryValue(route, "region"),
remoteWork: queryValue(route, 'remoteWork') || '', remoteWork: queryValue(route, "remoteWork") ?? "",
text: queryValue(route, 'text') text: queryValue(route, "text")
} }
const searchResult = await api.listings.search(searchParams, store.state.user as LogOnSuccess) const searchResult = await api.listings.search(searchParams, store.state.user as LogOnSuccess)
if (typeof searchResult === 'string') { if (typeof searchResult === "string") {
errors.value.push(searchResult) errors.value.push(searchResult)
} else if (searchResult === undefined) { } else if (searchResult === undefined) {
errors.value.push('The server returned a "Not Found" response (this should not happen)') errors.value.push(`The server returned a "Not Found" response (this should not happen)`)
} else { } else {
results.value = searchResult results.value = searchResult
searchParams.continentId = searchParams.continentId || '' searchParams.continentId = searchParams.continentId ?? ""
criteria.value = searchParams criteria.value = searchParams
} }
} finally { } finally {
@ -111,22 +104,14 @@ export default defineComponent({
errors.value = [] errors.value = []
results.value = [] results.value = []
} }
} }
watch(() => route.query, setUpPage, { immediate: true }) /** Refresh the page when the query string changes */
watch(() => route.query, setUpPage, { immediate: true })
return { /** Show or hide the search parameter panel */
errors, const toggleCollapse = (it : boolean) => { isCollapsed.value = it }
criteria,
isCollapsed, /** Execute a search */
toggleCollapse: (it : boolean) => { isCollapsed.value = it }, const doSearch = () => router.push({ query: { searched: "true", ...criteria.value } })
doSearch: () => router.push({ query: { searched: 'true', ...criteria.value } }),
searching,
searched,
results,
yesOrNo,
formatNeededBy
}
}
})
</script> </script>

View File

@ -3,71 +3,62 @@ article
page-title(:title="isNew ? 'Add a Job Listing' : 'Edit Job Listing'") 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-if="isNew") Add a Job Listing
h3.pb-3(v-else) Edit 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 .col-12.col-sm-10.col-md-8.col-lg-6
.form-floating .form-floating
input.form-control(type='text' id='title' :class="{ 'is-invalid': v$.title.$error }" maxlength='255' 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') v-model="v$.title.$model" placeholder="The title for the job listing")
#titleFeedback.invalid-feedback Please enter a 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 .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 .col-12.col-sm-6.col-md-4
continent-list(v-model='v$.continentId.$model' :isInvalid='v$.continentId.$error' continent-list(v-model="v$.continentId.$model" :isInvalid="v$.continentId.$error"
@touch='v$.continentId.$touch() || true') @touch="v$.continentId.$touch() || true")
.col-12.col-sm-6.col-md-8 .col-12.col-sm-6.col-md-8
.form-floating .form-floating
input.form-control(type='text' id='region' :class="{ 'is-invalid': v$.region.$error }" maxlength='255' 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.') v-model="v$.region.$model" placeholder="Country, state, geographic area, etc.")
#regionFeedback.invalid-feedback Please enter a region #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. .form-text Country, state, geographic area, etc.
.col-12: .form-check .col-12: .form-check
input.form-check-input(type='checkbox' id='isRemote' v-model='v$.remoteWork.$model') 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 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') markdown-editor(id="description" label="Job Description" v-model:text="v$.text.$model" :isInvalid="v$.text.$error")
.col-12.col-md-4: .form-floating .col-12.col-md-4: .form-floating
input.form-control(type='date' id='neededBy' v-model='v$.neededBy.$model' input.form-control(type="date" id="neededBy" v-model="v$.neededBy.$model"
placeholder='Date by which this position needs to be filled') placeholder="Date by which this position needs to be filled")
label(for='neededBy') Needed By label(for="neededBy") Needed By
.col-12 .col-12
p.text-danger(v-if='v$.$error') Please correct the errors above 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 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') maybe-save(:isShown="confirmNavShown" :toRoute="nextRoute" :saveAction="doSave" :validator="v$" @close="confirmClose")
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent, reactive, ref, Ref } from 'vue' import { computed, reactive, ref, Ref } from "vue"
import { onBeforeRouteLeave, RouteLocationNormalized, useRoute, useRouter } from 'vue-router' import { onBeforeRouteLeave, RouteLocationNormalized, useRoute, useRouter } from "vue-router"
import useVuelidate from '@vuelidate/core' import useVuelidate from "@vuelidate/core"
import { required } from '@vuelidate/validators' import { required } from "@vuelidate/validators"
import api, { Listing, ListingForm, LogOnSuccess } from '@/api' import api, { Listing, ListingForm, LogOnSuccess } from "@/api"
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue' import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
import { useStore } from '@/store' import { useStore } from "@/store"
import ContinentList from '@/components/ContinentList.vue' import ContinentList from "@/components/ContinentList.vue"
import LoadData from '@/components/LoadData.vue' import LoadData from "@/components/LoadData.vue"
import MarkdownEditor from '@/components/MarkdownEditor.vue' import MarkdownEditor from "@/components/MarkdownEditor.vue"
import MaybeSave from '@/components/MaybeSave.vue' import MaybeSave from "@/components/MaybeSave.vue"
export default defineComponent({ const store = useStore()
name: 'ListingEdit', const route = useRoute()
components: { const router = useRouter()
ContinentList,
LoadData,
MarkdownEditor,
MaybeSave
},
setup () {
const store = useStore()
const route = useRoute()
const router = useRouter()
/** The currently logged-on user */ /** The currently logged-on user */
const user = store.state.user as LogOnSuccess const user = store.state.user as LogOnSuccess
/** A new job listing */ /** A new job listing */
const newListing : Listing = { const newListing : Listing = {
id: '', id: '',
citizenId: user.citizenId, citizenId: user.citizenId,
createdOn: '', createdOn: '',
@ -80,19 +71,19 @@ export default defineComponent({
text: '', text: '',
neededBy: undefined, neededBy: undefined,
wasFilledHere: undefined wasFilledHere: undefined
} }
/** The backing object for the form */ /** The backing object for the form */
const listing = reactive(new ListingForm()) const listing = reactive(new ListingForm())
/** The ID of the listing requested */ /** The ID of the listing requested */
const id = route.params.id as string const id = route.params.id as string
/** Is this a new job listing? */ /** Is this a new job listing? */
const isNew = computed(() => id === 'new') const isNew = computed(() => id === "new")
/** Validation rules for the form */ /** Validation rules for the form */
const rules = computed(() => ({ const rules = computed(() => ({
id: { }, id: { },
title: { required }, title: { required },
continentId: { required }, continentId: { required },
@ -100,18 +91,18 @@ export default defineComponent({
remoteWork: { }, remoteWork: { },
text: { required }, text: { required },
neededBy: { } neededBy: { }
})) }))
/** Initialize form validation */ /** Initialize form validation */
const v$ = useVuelidate(rules, listing, { $lazy: true }) const v$ = useVuelidate(rules, listing, { $lazy: true })
/** Retrieve the listing being edited (or set up the form for a new listing) */ /** Retrieve the listing being edited (or set up the form for a new listing) */
const retrieveData = async (errors : string[]) => { const retrieveData = async (errors : string[]) => {
const listResult = isNew.value ? newListing : await api.listings.retreive(id, user) const listResult = isNew.value ? newListing : await api.listings.retreive(id, user)
if (typeof listResult === 'string') { if (typeof listResult === "string") {
errors.push(listResult) errors.push(listResult)
} else if (typeof listResult === 'undefined') { } else if (typeof listResult === "undefined") {
errors.push('Job listing not found') errors.push("Job listing not found")
} else { } else {
listing.id = listResult.id listing.id = listResult.id
listing.title = listResult.title listing.title = listResult.title
@ -121,48 +112,41 @@ export default defineComponent({
listing.text = listResult.text listing.text = listResult.text
listing.neededBy = listResult.neededBy listing.neededBy = listResult.neededBy
} }
} }
/** Save the job listing */ /** Save the job listing */
const saveListing = async (navigate : boolean) => { const saveListing = async (navigate : boolean) => {
v$.value.$touch() v$.value.$touch()
if (v$.value.$error) return if (v$.value.$error) return
const apiFunc = isNew.value ? api.listings.add : api.listings.update const apiFunc = isNew.value ? api.listings.add : api.listings.update
if (listing.neededBy === '') listing.neededBy = undefined if (listing.neededBy === "") listing.neededBy = undefined
const result = await apiFunc(listing, user) const result = await apiFunc(listing, user)
if (typeof result === 'string') { if (typeof result === "string") {
toastError(result, 'saving job listing') toastError(result, "saving job listing")
} else { } else {
toastSuccess(`Job Listing ${isNew.value ? 'Add' : 'Updat'}ed Successfully`) toastSuccess(`Job Listing ${isNew.value ? "Add" : "Updat"}ed Successfully`)
v$.value.$reset() v$.value.$reset()
if (navigate) router.push('/listings/mine') if (navigate) router.push("/listings/mine")
}
} }
}
/** Whether the navigation confirmation is shown */ /** Whether the navigation confirmation is shown */
const confirmNavShown = ref(false) const confirmNavShown = ref(false)
/** The "next" route (will be navigated or cleared) */ /** The "next" route (will be navigated or cleared) */
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined) const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
/** If the user has unsaved changes, give them an opportunity to save before moving on */ /** If the user has unsaved changes, give them an opportunity to save before moving on */
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
if (!v$.value.$anyDirty) return true if (!v$.value.$anyDirty) return true
nextRoute.value = to nextRoute.value = to
confirmNavShown.value = true confirmNavShown.value = true
return false return false
})
return {
isNew,
v$,
retrieveData,
saveListing,
confirmNavShown,
nextRoute,
doSave: async () => await saveListing(false),
confirmClose: () => { confirmNavShown.value = 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> </script>

View File

@ -1,84 +1,69 @@
<template lang="pug"> <template lang="pug">
article article
page-title(:title='pageTitle') page-title(:title="title")
load-data(:load='retrieveListing') load-data(:load="retrieveListing")
h3 {{it.listing.title}} h3 {{it.listing.title}}
h4.pb-3.text-muted {{it.continent.name}} / {{it.listing.region}} h4.pb-3.text-muted {{it.continent.name}} / {{it.listing.region}}
p p
template(v-if='it.listing.neededBy'). template(v-if="it.listing.neededBy").
#[strong #[em NEEDED BY {{neededBy(it.listing.neededBy)}}]] &bull; #[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 hr
div(v-html='details') div(v-html='details')
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent, ref, Ref } from 'vue' import { computed, ref, Ref } from "vue"
import { useRoute } from 'vue-router' import { useRoute } from "vue-router"
import { format } from 'date-fns'
import marked from 'marked'
import api, { Citizen, ListingForView, LogOnSuccess, markedOptions } from '@/api' import { formatNeededBy } from "./"
import { citizenName } from '@/App.vue' import api, { Citizen, ListingForView, LogOnSuccess } from "@/api"
import { useStore } from '@/store' import { citizenName } from "@/App.vue"
import LoadData from '@/components/LoadData.vue' import { toHtml } from "@/markdown"
import { useStore } from "@/store"
import LoadData from "@/components/LoadData.vue"
/** const store = useStore()
* Format the needed by date for display const route = useRoute()
*
* @param neededBy The defined needed by date
* @returns The date to display
*/
export function formatNeededBy (neededBy : string) : string {
return format(Date.parse(neededBy), 'PPP')
}
export default defineComponent({ /** The currently logged-on user */
name: 'ListingView', const user = store.state.user as LogOnSuccess
components: { LoadData },
setup () {
const store = useStore()
const route = useRoute()
/** The currently logged-on user */ /** The requested job listing */
const user = store.state.user as LogOnSuccess const it : Ref<ListingForView | undefined> = ref(undefined)
/** The requested job listing */ /** The citizen who posted this job listing */
const it : Ref<ListingForView | undefined> = ref(undefined) const citizen : Ref<Citizen | undefined> = ref(undefined)
/** The citizen who posted this job listing */ /** Retrieve the job listing and supporting data */
const citizen : Ref<Citizen | undefined> = ref(undefined) const retrieveListing = async (errors : string[]) => {
/** Retrieve the job listing and supporting data */
const retrieveListing = async (errors : string[]) => {
const listingResp = await api.listings.retreiveForView(route.params.id as string, user) const listingResp = await api.listings.retreiveForView(route.params.id as string, user)
if (typeof listingResp === 'string') { if (typeof listingResp === "string") {
errors.push(listingResp) errors.push(listingResp)
} else if (typeof listingResp === 'undefined') { } else if (typeof listingResp === "undefined") {
errors.push('Job Listing not found') errors.push("Job Listing not found")
} else { } else {
it.value = listingResp it.value = listingResp
const citizenResp = await api.citizen.retrieve(listingResp.listing.citizenId, user) const citizenResp = await api.citizen.retrieve(listingResp.listing.citizenId, user)
if (typeof citizenResp === 'string') { if (typeof citizenResp === "string") {
errors.push(citizenResp) errors.push(citizenResp)
} else if (typeof citizenResp === 'undefined') { } else if (typeof citizenResp === "undefined") {
errors.push('Listing Citizen not found (this should not happen)') errors.push("Listing Citizen not found (this should not happen)")
} else { } else {
citizen.value = citizenResp citizen.value = citizenResp
} }
} }
} }
return { /** The page title (changes once the listing is loaded) */
pageTitle: computed(() => it.value ? `${it.value.listing.title} | Job Listing` : 'Loading Job Listing...'), const title = computed(() => it.value ? `${it.value.listing.title} | Job Listing` : "Loading Job Listing...")
retrieveListing,
it, /** The HTML details of the job listing */
details: computed(() => marked(it.value?.listing.text || '', markedOptions)), const details = computed(() => toHtml(it.value?.listing.text ?? ""))
citizen,
profileUrl: computed(() => citizen.value ? `https://noagendasocial.com/@${citizen.value.naUser}` : ''), /** The NAS profile URL for the citizen who posted this job listing */
citizenName, const profileUrl = computed(() => citizen.value ? `https://noagendasocial.com/@${citizen.value.naUser}` : "")
neededBy: (nb : string) => formatNeededBy(nb).toUpperCase()
} /** The needed by date, formatted in SHOUTING MODE */
} const neededBy = (nb : string) => formatNeededBy(nb).toUpperCase()
})
</script> </script>

View File

@ -1,18 +1,18 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='My Job Listings') page-title(title="My Job Listings")
h3.pb-3 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") load-data(:load="getListings")
table.table.table-sm.table-hover.pt-3(v-if='listings.length > 0') table.table.table-sm.table-hover.pt-3(v-if='listings.length > 0')
thead: tr thead: tr
th(scope='col') Action th(scope="col") Action
th(scope='col') Title th(scope="col") Title
th(scope='col') Continent / Region th(scope="col") Continent / Region
th(scope='col') Created th(scope="col") Created
th(scope='col') Updated th(scope="col") Updated
tbody: tr(v-for='it in listings' :key='it.listing.id') 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.listing.title}}
td {{it.continent.name}} / {{it.listing.region}} td {{it.continent.name}} / {{it.listing.region}}
td: full-date-time(:date='it.listing.createdOn') td: full-date-time(:date='it.listing.createdOn')
@ -20,42 +20,28 @@ article
p.fst-italic(v-else) No job listings found p.fst-italic(v-else) No job listings found
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, Ref, ref } from 'vue' import { Ref, ref } from "vue"
import api, { ListingForView, LogOnSuccess } from '@/api' import api, { ListingForView, LogOnSuccess } from "@/api"
import { useStore } from '@/store' import { useStore } from "@/store"
import FullDateTime from '@/components/FullDateTime.vue' import FullDateTime from "@/components/FullDateTime.vue"
import LoadData from '@/components/LoadData.vue' import LoadData from "@/components/LoadData.vue"
export default defineComponent({ const store = useStore()
name: 'MyListings',
components: {
FullDateTime,
LoadData
},
setup () {
const store = useStore()
/** The listings for the user */ /** The listings for the user */
const listings : Ref<ListingForView[]> = ref([]) const listings : Ref<ListingForView[]> = ref([])
/** Retrieve the job listing posted by the current citizen */ /** Retrieve the job listing posted by the current citizen */
const getListings = async (errors : string[]) => { const getListings = async (errors : string[]) => {
const listResult = await api.listings.mine(store.state.user as LogOnSuccess) const listResult = await api.listings.mine(store.state.user as LogOnSuccess)
if (typeof listResult === 'string') { if (typeof listResult === "string") {
errors.push(listResult) errors.push(listResult)
} else if (typeof listResult === 'undefined') { } else if (typeof listResult === "undefined") {
errors.push('API call returned 404 (this should not happen)') errors.push("API call returned 404 (this should not happen)")
} else { } else {
listings.value = listResult listings.value = listResult
} }
} }
return {
getListings,
listings
}
}
})
</script> </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,106 +1,97 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Search Profiles') page-title(title="Search Profiles")
h3.pb-3 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. 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') collapse-panel(headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse")
profile-search-form(v-model='criteria' @search='doSearch') profile-search-form(v-model="criteria" @search="doSearch")
error-list(:errors='errors') error-list(:errors="errors")
p.pt-3(v-if='searching') Searching profiles&hellip; p.pt-3(v-if="searching") Searching profiles&hellip;
template(v-else) 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 thead: tr
th(scope='col') Profile th(scope="col") Profile
th(scope='col') Name th(scope="col") Name
th.text-center(scope='col') Seeking? th.text-center(scope="col") Seeking?
th.text-center(scope='col') Remote? th.text-center(scope="col") Remote?
th.text-center(scope='col') Full-Time? th.text-center(scope="col") Full-Time?
th(scope='col') Last Updated th(scope="col") Last Updated
tbody: tr(v-for='profile in results' :key='profile.citzenId') tbody: tr(v-for="profile in results" :key="profile.citzenId")
td: router-link(:to='`/profile/${profile.citizenId}/view`') View td: router-link(:to="`/profile/${profile.citizenId}/view`") View
td(:class="{ 'font-weight-bold' : profile.seekingEmployment }") {{profile.displayName}} td(:class="{ 'font-weight-bold' : profile.seekingEmployment }") {{profile.displayName}}
td.text-center {{yesOrNo(profile.seekingEmployment)}} td.text-center {{yesOrNo(profile.seekingEmployment)}}
td.text-center {{yesOrNo(profile.remoteWork)}} td.text-center {{yesOrNo(profile.remoteWork)}}
td.text-center {{yesOrNo(profile.fullTime)}} td.text-center {{yesOrNo(profile.fullTime)}}
td: full-date(:date='profile.lastUpdatedOn') td: full-date(:date="profile.lastUpdatedOn")
p.pt-3(v-else-if='searched') No results found for the specified criteria p.pt-3(v-else-if="searched") No results found for the specified criteria
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, Ref, ref, watch } from 'vue' import { defineComponent, Ref, ref, watch } from "vue"
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from "vue-router"
import { yesOrNo } from '@/App.vue' import { yesOrNo } from "@/App.vue"
import api, { LogOnSuccess, ProfileSearch, ProfileSearchResult } from '@/api' import api, { LogOnSuccess, ProfileSearch, ProfileSearchResult } from "@/api"
import { queryValue } from '@/router' import { queryValue } from "@/router"
import { useStore } from '@/store' import { useStore } from "@/store"
import CollapsePanel from '@/components/CollapsePanel.vue' import CollapsePanel from "@/components/CollapsePanel.vue"
import ErrorList from '@/components/ErrorList.vue' import ErrorList from "@/components/ErrorList.vue"
import FullDate from '@/components/FullDate.vue' import FullDate from "@/components/FullDate.vue"
import ProfileSearchForm from '@/components/profile/SearchForm.vue' import ProfileSearchForm from "@/components/profile/SearchForm.vue"
export default defineComponent({ const store = useStore()
name: 'ProfileSearch', const route = useRoute()
components: { const router = useRouter()
CollapsePanel,
ErrorList,
FullDate,
ProfileSearchForm
},
setup () {
const store = useStore()
const route = useRoute()
const router = useRouter()
/** Any errors encountered while retrieving data */ /** Any errors encountered while retrieving data */
const errors : Ref<string[]> = ref([]) const errors : Ref<string[]> = ref([])
/** Whether we are currently searching (retrieving data) */ /** Whether we are currently searching (retrieving data) */
const searching = ref(false) const searching = ref(false)
/** Whether a search has been performed on this page since it has been loaded */ /** Whether a search has been performed on this page since it has been loaded */
const searched = ref(false) const searched = ref(false)
/** An empty set of search criteria */ /** An empty set of search criteria */
const emptyCriteria = { const emptyCriteria = {
continentId: '', continentId: '',
skill: undefined, skill: undefined,
bioExperience: undefined, bioExperience: undefined,
remoteWork: '' remoteWork: ''
} }
/** The search criteria being built from the page */ /** The search criteria being built from the page */
const criteria : Ref<ProfileSearch> = ref(emptyCriteria) const criteria : Ref<ProfileSearch> = ref(emptyCriteria)
/** The current search results */ /** The current search results */
const results : Ref<ProfileSearchResult[]> = ref([]) const results : Ref<ProfileSearchResult[]> = ref([])
/** Whether the search criteria should be collapsed */ /** Whether the search criteria should be collapsed */
const isCollapsed = ref(searched.value && results.value.length > 0) const isCollapsed = ref(searched.value && results.value.length > 0)
/** Set up the page to match its requested state */ /** Set up the page to match its requested state */
const setUpPage = async () => { const setUpPage = async () => {
if (queryValue(route, 'searched') === 'true') { if (queryValue(route, "searched") === "true") {
searched.value = true searched.value = true
try { try {
searching.value = true searching.value = true
// Hold variable for ensuring continent ID is not undefined here, but excluded from search payload // Hold variable for ensuring continent ID is not undefined here, but excluded from search payload
const contId = queryValue(route, 'continentId') const contId = queryValue(route, "continentId")
const searchParams : ProfileSearch = { const searchParams : ProfileSearch = {
continentId: contId === '' ? undefined : contId, continentId: contId === "" ? undefined : contId,
skill: queryValue(route, 'skill'), skill: queryValue(route, "skill"),
bioExperience: queryValue(route, 'bioExperience'), bioExperience: queryValue(route, "bioExperience"),
remoteWork: queryValue(route, 'remoteWork') || '' remoteWork: queryValue(route, "remoteWork") ?? ""
} }
const searchResult = await api.profile.search(searchParams, store.state.user as LogOnSuccess) const searchResult = await api.profile.search(searchParams, store.state.user as LogOnSuccess)
if (typeof searchResult === 'string') { if (typeof searchResult === "string") {
errors.value.push(searchResult) errors.value.push(searchResult)
} else if (searchResult === undefined) { } else if (searchResult === undefined) {
errors.value.push('The server returned a "Not Found" response (this should not happen)') errors.value.push(`The server returned a "Not Found" response (this should not happen)`)
} else { } else {
results.value = searchResult results.value = searchResult
searchParams.continentId = searchParams.continentId || '' searchParams.continentId = searchParams.continentId ?? ""
criteria.value = searchParams criteria.value = searchParams
} }
} finally { } finally {
@ -113,21 +104,14 @@ export default defineComponent({
errors.value = [] errors.value = []
results.value = [] results.value = []
} }
} }
watch(() => route.query, setUpPage, { immediate: true }) /** Refresh the page when the query string changes */
watch(() => route.query, setUpPage, { immediate: true })
return { /** Show and hide the search parameter panel */
errors, const toggleCollapse = (it : boolean) => { isCollapsed.value = it }
criteria,
isCollapsed, /** Execute a search */
toggleCollapse: (it : boolean) => { isCollapsed.value = it }, const doSearch = () => router.push({ query: { searched: 'true', ...criteria.value } })
doSearch: () => router.push({ query: { searched: 'true', ...criteria.value } }),
searching,
searched,
results,
yesOrNo
}
}
})
</script> </script>

View File

@ -1,90 +1,83 @@
<template lang="pug"> <template lang="pug">
article article
page-title(:title='pageTitle') page-title(:title="title")
load-data(:load='retrieveProfile') load-data(:load="retrieveProfile")
h2: a(:href='it.citizen.profileUrl' target='_blank') {{citizenName(it.citizen)}} h2: a(:href="it.citizen.profileUrl" target="_blank") {{citizenName(it.citizen)}}
h4.pb-3 {{it.continent.name}}, {{it.profile.region}} h4.pb-3 {{it.continent.name}}, {{it.profile.region}}
p(v-html='workTypes') p(v-html="workTypes")
hr hr
div(v-html='bioHtml') div(v-html="bioHtml")
template(v-if='it.profile.skills.length > 0') template(v-if="it.profile.skills.length > 0")
hr hr
h4.pb-3 Skills h4.pb-3 Skills
ul ul
li(v-for='(skill, idx) in it.profile.skills' :key='idx'). li(v-for="(skill, idx) in it.profile.skills" :key="idx").
{{skill.description}}#[template(v-if='skill.notes') ({{skill.notes}})] {{skill.description}}#[template(v-if="skill.notes") &nbsp;({{skill.notes}})]
template(v-if='it.profile.experience') template(v-if="it.profile.experience")
hr hr
h4.pb-3 Experience / Employment History h4.pb-3 Experience / Employment History
div(v-html='expHtml') div(v-html="expHtml")
template(v-if='user.citizenId === it.citizen.id') template(v-if="user.citizenId === it.citizen.id")
br br
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> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent, ref, Ref } from 'vue' import { computed, ref, Ref } from "vue"
import { useRoute } from 'vue-router' import { useRoute } from "vue-router"
import marked from 'marked'
import api, { LogOnSuccess, markedOptions, ProfileForView } from '@/api' import api, { LogOnSuccess, ProfileForView } from "@/api"
import { citizenName } from '@/App.vue' import { citizenName } from "@/App.vue"
import { useStore } from '@/store' import { toHtml } from "@/markdown"
import LoadData from '@/components/LoadData.vue' import { useStore } from "@/store"
import LoadData from "@/components/LoadData.vue"
export default defineComponent({ const store = useStore()
name: 'ProfileView', const route = useRoute()
components: { LoadData },
setup () {
const store = useStore()
const route = useRoute()
/** The currently logged-on user */ /** The currently logged-on user */
const user = store.state.user as LogOnSuccess const user = store.state.user as LogOnSuccess
/** The requested profile */ /** The requested profile */
const it : Ref<ProfileForView | undefined> = ref(undefined) const it : Ref<ProfileForView | undefined> = ref(undefined)
/** The work types for the top of the page */ /** The work types for the top of the page */
const workTypes = computed(() => { const workTypes = computed(() => {
const parts : string[] = [] const parts : string[] = []
if (it.value) { if (it.value) {
const p = it.value.profile const p = it.value.profile
if (p.seekingEmployment) { if (p.seekingEmployment) {
parts.push('<strong><em>CURRENTLY SEEKING EMPLOYMENT</em></strong>') parts.push("<strong><em>CURRENTLY SEEKING EMPLOYMENT</em></strong>")
} else { } else {
parts.push('Not actively seeking employment') parts.push("Not actively seeking employment")
} }
parts.push(`${p.fullTime ? 'I' : 'Not i'}nterested in full-time employment`) parts.push(`${p.fullTime ? "I" : "Not i"}nterested in full-time employment`)
parts.push(`${p.remoteWork ? 'I' : 'Not i'}nterested in remote opportunities`) parts.push(`${p.remoteWork ? "I" : "Not i"}nterested in remote opportunities`)
} }
return parts.join(' &bull; ') return parts.join(" &bull; ")
}) })
/** Retrieve the profile and supporting data */ /** Retrieve the profile and supporting data */
const retrieveProfile = async (errors : string[]) => { const retrieveProfile = async (errors : string[]) => {
const profileResp = await api.profile.retreiveForView(route.params.id as string, user) const profileResp = await api.profile.retreiveForView(route.params.id as string, user)
if (typeof profileResp === 'string') { if (typeof profileResp === "string") {
errors.push(profileResp) errors.push(profileResp)
} else if (typeof profileResp === 'undefined') { } else if (typeof profileResp === "undefined") {
errors.push('Profile not found') errors.push("Profile not found")
} else { } else {
it.value = profileResp it.value = profileResp
} }
} }
return { /** The title of the page (changes once the profile is loaded) */
pageTitle: computed(() => const title = computed(() => it.value
it.value ? `Employment profile for ${citizenName(it.value.citizen)}` : 'Loading Profile...'), ? `Employment profile for ${citizenName(it.value.citizen)}`
user, : "Loading Profile...")
retrieveProfile,
citizenName, /** The HTML version of the citizen's professional biography */
it, const bioHtml = computed(() => toHtml(it.value?.profile.biography ?? ""))
workTypes,
bioHtml: computed(() => marked(it.value?.profile.biography || '', markedOptions)), /** The HTML version of the citizens Experience section */
expHtml: computed(() => marked(it.value?.profile.experience || '', markedOptions)) const expHtml = computed(() => toHtml(it.value?.profile.experience ?? ""))
}
}
})
</script> </script>

View File

@ -1,100 +1,93 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='People Seeking Work') page-title(title="People Seeking Work")
h3.pb-3 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. 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') collapse-panel(headerText="Search Criteria" :collapsed="isCollapsed" @toggle="toggleCollapse")
profile-public-search-form(v-model='criteria' @search='doSearch') profile-public-search-form(v-model="criteria" @search="doSearch")
error-list(:errors='errors') error-list(:errors="errors")
p(v-if='searching') Searching profiles&hellip; p(v-if="searching") Searching profiles&hellip;
template(v-else) template(v-else)
template(v-if='results.length > 0') template(v-if="results.length > 0")
p.pb-3.pt-3. p.pb-3.pt-3.
These profiles match your search criteria. To learn more about these people, join the merry band of human 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 table.table.table-sm.table-hover
thead: tr thead: tr
th(scope='col') Continent th(scope="col") Continent
th.text-center(scope='col') Region th.text-center(scope="col") Region
th.text-center(scope='col') Remote? th.text-center(scope="col") Remote?
th.text-center(scope='col') Skills th.text-center(scope="col") Skills
tbody: tr(v-for='(profile, idx) in results' :key='idx') tbody: tr(v-for="(profile, idx) in results" :key="idx")
td {{profile.continent}} td {{profile.continent}}
td {{profile.region}} td {{profile.region}}
td.text-center {{yesOrNo(profile.remoteWork)}} td.text-center {{yesOrNo(profile.remoteWork)}}
td: template(v-for='(skill, idx) in profile.skills' :key='idx') {{skill}}#[br] 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 p.pt-3(v-else-if="searched") No results found for the specified criteria
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, Ref, ref, watch } from 'vue' import { Ref, ref, watch } from "vue"
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from "vue-router"
import { yesOrNo } from '@/App.vue' import { yesOrNo } from "@/App.vue"
import api, { PublicSearch, PublicSearchResult } from '@/api' import api, { PublicSearch, PublicSearchResult } from "@/api"
import { queryValue } from '@/router' import { queryValue } from "@/router"
import CollapsePanel from '@/components/CollapsePanel.vue' import CollapsePanel from "@/components/CollapsePanel.vue"
import ErrorList from '@/components/ErrorList.vue' import ErrorList from "@/components/ErrorList.vue"
import ProfilePublicSearchForm from '@/components/profile/PublicSearchForm.vue' import ProfilePublicSearchForm from "@/components/profile/PublicSearchForm.vue"
export default defineComponent({ const route = useRoute()
components: { const router = useRouter()
CollapsePanel,
ErrorList,
ProfilePublicSearchForm
},
setup () {
const route = useRoute()
const router = useRouter()
/** Whether a search has been performed */ /** Whether a search has been performed */
const searched = ref(false) const searched = ref(false)
/** Indicates whether a request for matching profiles is in progress */ /** Indicates whether a request for matching profiles is in progress */
const searching = ref(false) const searching = ref(false)
/** Error messages encountered while searching for profiles */ /** Error messages encountered while searching for profiles */
const errors : Ref<string[]> = ref([]) const errors : Ref<string[]> = ref([])
/** An empty set of search criteria */ /** An empty set of search criteria */
const emptyCriteria = { const emptyCriteria = {
continentId: '', continentId: '',
region: undefined, region: undefined,
skill: undefined, skill: undefined,
remoteWork: '' remoteWork: ''
} }
/** The search criteria being built from the page */ /** The search criteria being built from the page */
const criteria : Ref<PublicSearch> = ref(emptyCriteria) const criteria : Ref<PublicSearch> = ref(emptyCriteria)
/** The search results */ /** The search results */
const results : Ref<PublicSearchResult[]> = ref([]) const results : Ref<PublicSearchResult[]> = ref([])
/** Whether the search results are collapsed */ /** Whether the search results are collapsed */
const isCollapsed = ref(searched.value && results.value.length > 0) const isCollapsed = ref(searched.value && results.value.length > 0)
/** Set up the page to match its requested state */ /** Set up the page to match its requested state */
const setUpPage = async () => { const setUpPage = async () => {
if (queryValue(route, 'searched') === 'true') { if (queryValue(route, "searched") === "true") {
searched.value = true searched.value = true
try { try {
searching.value = true searching.value = true
const contId = queryValue(route, 'continentId') const contId = queryValue(route, "continentId")
const searchParams : PublicSearch = { const searchParams : PublicSearch = {
continentId: contId === '' ? undefined : contId, continentId: contId === "" ? undefined : contId,
region: queryValue(route, 'region'), region: queryValue(route, "region"),
skill: queryValue(route, 'skill'), skill: queryValue(route, "skill"),
remoteWork: queryValue(route, 'remoteWork') || '' remoteWork: queryValue(route, "remoteWork") ?? ""
} }
const searchResult = await api.profile.publicSearch(searchParams) const searchResult = await api.profile.publicSearch(searchParams)
if (typeof searchResult === 'string') { if (typeof searchResult === "string") {
errors.value.push(searchResult) errors.value.push(searchResult)
} else if (searchResult === undefined) { } else if (searchResult === undefined) {
errors.value.push('The server returned a "Not Found" response (this should not happen)') errors.value.push(`The server returned a "Not Found" response (this should not happen)`)
} else { } else {
results.value = searchResult results.value = searchResult
searchParams.continentId = searchParams.continentId || '' searchParams.continentId = searchParams.continentId ?? ""
criteria.value = searchParams criteria.value = searchParams
} }
} finally { } finally {
@ -107,21 +100,14 @@ export default defineComponent({
errors.value = [] errors.value = []
results.value = [] results.value = []
} }
} }
watch(() => route.query, setUpPage, { immediate: true }) /** Refresh the page when the query string changes */
watch(() => route.query, setUpPage, { immediate: true })
return { /** Open and closed the search parameter panel */
errors, const toggleCollapse = (it : boolean) => { isCollapsed.value = it }
criteria,
isCollapsed, /** Execute a search */
toggleCollapse: (it : boolean) => { isCollapsed.value = it }, const doSearch = () => router.push({ query: { searched: 'true', ...criteria.value } })
doSearch: () => router.push({ query: { searched: 'true', ...criteria.value } }),
searching,
searched,
results,
yesOrNo
}
}
})
</script> </script>

View File

@ -1,13 +1,13 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Account Deletion Options') page-title(title="Account Deletion Options")
h3.pb-3 Account Deletion Options h3.pb-3 Account Deletion Options
h4.pb-3 Option 1 &ndash; Delete Your Profile h4.pb-3 Option 1 &ndash; Delete Your Profile
p. p.
Utilizing this option will remove your current employment profile and skills. This will preserve any success stories 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 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). 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 hr
h4.pb-3 Option 2 &ndash; Delete Your Account h4.pb-3 Option 2 &ndash; Delete Your Account
p. 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 (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 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.) #[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> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { useRouter } from "vue-router"
import { useRouter } from 'vue-router' import api, { LogOnSuccess } from "@/api"
import api, { LogOnSuccess } from '@/api' import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue' import { useStore } from "@/store"
import { useStore } from '@/store' </script>
export default defineComponent({ <script setup lang="ts">
name: 'DeletionOptions', const store = useStore()
setup () { const router = useRouter()
const store = useStore()
const router = useRouter() /** Delete the profile only; redirect to home page on success */
const deleteProfile = async () => {
/** Delete the profile only; redirect to home page on success */ const resp = await api.profile.delete(store.state.user as LogOnSuccess)
const deleteProfile = async () => { if (typeof resp === "string") {
const resp = await api.profile.delete(store.state.user as LogOnSuccess) toastError(resp, "Deleting Profile")
if (typeof resp === 'string') { } else {
toastError(resp, 'Deleting Profile') toastSuccess("Profile Deleted Successfully")
} else { router.push("/citizen/dashboard")
toastSuccess('Profile Deleted Successfully') }
router.push('/citizen/dashboard') }
}
} /** Delete everything pertaining to the user's account */
const deleteAccount = async () => {
/** Delete everything pertaining to the user's account */ const resp = await api.citizen.delete(store.state.user as LogOnSuccess)
const deleteAccount = async () => { if (typeof resp === "string") {
const resp = await api.citizen.delete(store.state.user as LogOnSuccess) toastError(resp, "Deleting Account")
if (typeof resp === 'string') { } else {
toastError(resp, 'Deleting Account') store.commit("clearUser")
} else { toastSuccess("Account Deleted Successfully")
store.commit('clearUser') router.push("/so-long/success")
toastSuccess('Account Deleted Successfully') }
router.push('/so-long/success') }
}
}
return {
deleteProfile,
deleteAccount
}
}
})
</script> </script>

View File

@ -1,10 +1,10 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Account Deletion Success') page-title(title="Account Deletion Success")
h3.pb-3 Account Deletion Success h3.pb-3 Account Deletion Success
p. p.
Your account has been successfully deleted. To revoke the permissions you have previously granted to this 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 #[strong &times; Revoke]. Otherwise, clicking &ldquo;Log On&rdquo; in the left-hand menu will create a new, empty
account without prompting you further. account without prompting you further.
p Thank you for participating, and thank you for your courage. #GitmoNation p Thank you for participating, and thank you for your courage. #GitmoNation

View File

@ -1,143 +1,128 @@
<template lang="pug"> <template lang="pug">
article article
page-title(:title='title') page-title(:title="title")
h3.pb-3 {{title}} h3.pb-3 {{title}}
load-data(:load='retrieveStory') load-data(:load="retrieveStory")
p(v-if='isNew'). p(v-if="isNew").
Congratulations on your employment! Your fellow citizens would enjoy hearing how it all came about; tell us 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.)] about it below! #[em (These will be visible to other users, but not to the general public.)]
form.row.g-3 form.row.g-3
.col-12: .form-check .col-12: .form-check
input.form-check-input(type='checkbox' id='fromHere' v-model='v$.fromHere.$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 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') markdown-editor(id="story" label="The Success Story" v-model:text="v$.story.$model")
.col-12 .col-12
button.btn.btn-primary(type='submit' @click.prevent='saveStory(true)'). button.btn.btn-primary(type="submit" @click.prevent="saveStory(true)").
#[icon(icon='content-save-outline')]&nbsp; Save #[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.) 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') maybe-save(:isShown="confirmNavShown" :toRoute="nextRoute" :saveAction="doSave" :validator="v$" @close="confirmClose")
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent, reactive, ref, Ref } from 'vue' import { computed, reactive, ref, Ref } from "vue"
import { onBeforeRouteLeave, RouteLocationNormalized, useRoute, useRouter } from 'vue-router' import { onBeforeRouteLeave, RouteLocationNormalized, useRoute, useRouter } from "vue-router"
import useVuelidate from '@vuelidate/core' import useVuelidate from "@vuelidate/core"
import api, { LogOnSuccess, StoryForm } from '@/api'
import { toastError, toastSuccess } from '@/components/layout/AppToaster.vue'
import { useStore } from '@/store'
import LoadData from '@/components/LoadData.vue' import api, { LogOnSuccess, StoryForm } from "@/api"
import MarkdownEditor from '@/components/MarkdownEditor.vue' import { toastError, toastSuccess } from "@/components/layout/AppToaster.vue"
import MaybeSave from '@/components/MaybeSave.vue' import { useStore } from "@/store"
export default defineComponent({ import LoadData from "@/components/LoadData.vue"
name: 'StoryEdit', import MarkdownEditor from "@/components/MarkdownEditor.vue"
components: { import MaybeSave from "@/components/MaybeSave.vue"
LoadData,
MarkdownEditor,
MaybeSave
},
setup () {
const store = useStore()
const route = useRoute()
const router = useRouter()
/** The currently logged-on user */ const store = useStore()
const user = store.state.user as LogOnSuccess const route = useRoute()
const router = useRouter()
/** The ID of the story being edited */ /** The currently logged-on user */
const id = route.params.id as string const user = store.state.user as LogOnSuccess
/** Whether this is a new story */ /** The ID of the story being edited */
const isNew = computed(() => id === 'new') const id = route.params.id as string
/** The page title */ /** Whether this is a new story */
const title = computed(() => isNew.value ? 'Tell Your Success Story' : 'Edit Success Story') const isNew = computed(() => id === "new")
/** The form for editing the story */ /** The page title */
const story = reactive(new StoryForm()) const title = computed(() => isNew.value ? "Tell Your Success Story" : "Edit Success Story")
/** Validator rules */ /** The form for editing the story */
const rules = computed(() => ({ const story = reactive(new StoryForm())
/** Validator rules */
const rules = computed(() => ({
fromHere: { }, fromHere: { },
story: { } story: { }
})) }))
/** The validator */ /** The validator */
const v$ = useVuelidate(rules, story, { $lazy: true }) const v$ = useVuelidate(rules, story, { $lazy: true })
/** Retrieve the specified story */ /** Retrieve the specified story */
const retrieveStory = async (errors : string[]) => { const retrieveStory = async (errors : string[]) => {
if (isNew.value) { if (isNew.value) {
story.id = 'new' story.id = "new"
} else { } else {
const storyResult = await api.success.retrieve(id, user) const storyResult = await api.success.retrieve(id, user)
if (typeof storyResult === 'string') { if (typeof storyResult === "string") {
errors.push(storyResult) errors.push(storyResult)
} else if (typeof storyResult === 'undefined') { } else if (typeof storyResult === "undefined") {
errors.push('Story not found') errors.push("Story not found")
} else if (storyResult.citizenId !== user.citizenId) { } else if (storyResult.citizenId !== user.citizenId) {
errors.push('Quit messing around') errors.push("Quit messing around")
} else { } else {
story.id = storyResult.id story.id = storyResult.id
story.fromHere = storyResult.fromHere story.fromHere = storyResult.fromHere
story.story = storyResult.story || '' story.story = storyResult.story ?? ""
}
} }
} }
}
/** Save the success story */ /** Save the success story */
const saveStory = async (navigate : boolean) => { const saveStory = async (navigate : boolean) => {
const saveResult = await api.success.save(story, user) const saveResult = await api.success.save(story, user)
if (typeof saveResult === 'string') { if (typeof saveResult === "string") {
toastError(saveResult, 'saving success story') toastError(saveResult, "saving success story")
} else { } else {
if (isNew.value) { if (isNew.value) {
const foundResult = await api.profile.markEmploymentFound(user) const foundResult = await api.profile.markEmploymentFound(user)
if (typeof foundResult === 'string') { if (typeof foundResult === "string") {
toastError(foundResult, 'clearing employment flag') toastError(foundResult, "clearing employment flag")
} else { } else {
toastSuccess('Success Story saved and Seeking Employment flag cleared successfully') toastSuccess("Success Story saved and Seeking Employment flag cleared successfully")
v$.value.$reset() v$.value.$reset()
if (navigate) { if (navigate) {
router.push('/success-story/list') router.push("/success-story/list")
} }
} }
} else { } else {
toastSuccess('Success Story saved successfully') toastSuccess("Success Story saved successfully")
v$.value.$reset() v$.value.$reset()
if (navigate) { if (navigate) {
router.push('/success-story/list') router.push("/success-story/list")
}
} }
} }
} }
}
/** Whether the navigation confirmation is shown */ /** Whether the navigation confirmation is shown */
const confirmNavShown = ref(false) const confirmNavShown = ref(false)
/** The "next" route (will be navigated or cleared) */ /** The "next" route (will be navigated or cleared) */
const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined) const nextRoute : Ref<RouteLocationNormalized | undefined> = ref(undefined)
/** Prompt for save if the user navigates away with unsaved changes */ /** Prompt for save if the user navigates away with unsaved changes */
onBeforeRouteLeave(async (to, from) => { // eslint-disable-line onBeforeRouteLeave(async (to, from) => { // eslint-disable-line
if (!v$.value.$anyDirty) return true if (!v$.value.$anyDirty) return true
nextRoute.value = to nextRoute.value = to
confirmNavShown.value = true confirmNavShown.value = true
return false return false
})
return {
title,
isNew,
retrieveStory,
v$,
saveStory,
confirmNavShown,
nextRoute,
doSave: async () => await saveStory(false),
confirmClose: () => { confirmNavShown.value = 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> </script>

View File

@ -1,68 +1,53 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Success Stories') page-title(title="Success Stories")
h3.pb-3 Success Stories h3.pb-3 Success Stories
load-data(:load='retrieveStories') load-data(:load="retrieveStories")
table.table.table-sm.table-hover(v-if='stories?.length > 0') table.table.table-sm.table-hover(v-if="stories?.length > 0")
thead: tr thead: tr
th(scope='col') Story th(scope="col") Story
th(scope='col') From th(scope="col") From
th(scope='col') Found Here? th(scope="col") Found Here?
th(scope='col') Recorded On th(scope="col") Recorded On
tbody: tr(v-for='story in stories' :key='story.id') tbody: tr(v-for="story in stories" :key="story.id")
td 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 em(v-else) None
template(v-if='story.citizenId === user.citizenId') template(v-if="story.citizenId === user.citizenId")
| ~ #[router-link(:to='`/success-story/${story.id}/edit`') Edit] | ~ #[router-link(:to="`/success-story/${story.id}/edit`") Edit]
td {{story.citizenName}} td {{story.citizenName}}
td td
strong(v-if='story.fromHere') Yes strong(v-if="story.fromHere") Yes
template(v-else) No 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)] p(v-else) There are no success stories recorded #[em (yet)]
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, ref, Ref } from 'vue' import { ref, Ref } from "vue"
import api, { LogOnSuccess, StoryEntry } from '@/api' import api, { LogOnSuccess, StoryEntry } from "@/api"
import { useStore } from '@/store' import { useStore } from "@/store"
import FullDate from '@/components/FullDate.vue' import FullDate from "@/components/FullDate.vue"
import LoadData from '@/components/LoadData.vue' import LoadData from "@/components/LoadData.vue"
export default defineComponent({ const store = useStore()
name: 'StoryList',
components: {
FullDate,
LoadData
},
setup () {
const store = useStore()
/** The currently logged-on user */ /** The currently logged-on user */
const user = store.state.user as LogOnSuccess const user = store.state.user as LogOnSuccess
/** The success stories to be displayed */ /** The success stories to be displayed */
const stories : Ref<StoryEntry[] | undefined> = ref(undefined) const stories : Ref<StoryEntry[] | undefined> = ref(undefined)
/** Get all currently recorded stories */ /** Get all currently recorded stories */
const retrieveStories = async (errors : string[]) => { const retrieveStories = async (errors : string[]) => {
const listResult = await api.success.list(user) const listResult = await api.success.list(user)
if (typeof listResult === 'string') { if (typeof listResult === "string") {
errors.push(listResult) errors.push(listResult)
} else if (typeof listResult === 'undefined') { } else if (typeof listResult === "undefined") {
stories.value = [] stories.value = []
} else { } else {
stories.value = listResult stories.value = listResult
} }
} }
return {
retrieveStories,
stories,
user
}
}
})
</script> </script>

View File

@ -1,71 +1,60 @@
<template lang="pug"> <template lang="pug">
article article
page-title(title='Success Story') page-title(title="Success Story")
load-data(:load='retrieveStory') load-data(:load="retrieveStory")
h3.pb-3 {{citizenName}}&rsquo;s Success Story h3.pb-3 {{citizenName}}&rsquo;s Success Story
h4.text-muted: full-date-time(:date='story.recordedOn') h4.text-muted: full-date-time(:date="story.recordedOn")
p.fst-italic(v-if='story.fromHere'): strong Found via Jobs, Jobs, Jobs p.fst-italic(v-if="story.fromHere"): strong Found via Jobs, Jobs, Jobs
hr hr
div(v-if='story.story' v-html='successStory') div(v-if="story.story" v-html="successStory")
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { computed, defineComponent, Ref, ref } from 'vue' import { computed, Ref, ref } from "vue"
import { useRoute } from 'vue-router' import { useRoute } from "vue-router"
import marked from 'marked'
import api, { LogOnSuccess, markedOptions, Success } from '@/api'
import { useStore } from '@/store'
import FullDateTime from '@/components/FullDateTime.vue' import api, { LogOnSuccess, Success } from "@/api"
import LoadData from '@/components/LoadData.vue' import { citizenName as citName } from "@/App.vue"
import { toHtml } from '@/markdown'
import { useStore } from "@/store"
export default defineComponent({ import FullDateTime from "@/components/FullDateTime.vue"
name: 'StoryView', import LoadData from "@/components/LoadData.vue"
components: {
FullDateTime,
LoadData
},
setup () {
const store = useStore()
const route = useRoute()
/** The currently logged-on user */ const store = useStore()
const user = store.state.user as LogOnSuccess const route = useRoute()
/** The story to be displayed */ /** The currently logged-on user */
const story : Ref<Success | undefined> = ref(undefined) const user = store.state.user as LogOnSuccess
/** The citizen's name (real, display, or NAS, whichever is found first) */ /** The story to be displayed */
const citizenName = ref('') const story : Ref<Success | undefined> = ref(undefined)
/** Retrieve the success story */ /** The citizen's name (real, display, or NAS, whichever is found first) */
const retrieveStory = async (errors : string []) => { const citizenName = ref("")
/** Retrieve the success story */
const retrieveStory = async (errors : string []) => {
const storyResponse = await api.success.retrieve(route.params.id as string, user) const storyResponse = await api.success.retrieve(route.params.id as string, user)
if (typeof storyResponse === 'string') { if (typeof storyResponse === "string") {
errors.push(storyResponse) errors.push(storyResponse)
return return
} }
if (typeof storyResponse === 'undefined') { if (typeof storyResponse === "undefined") {
errors.push('Success story not found') errors.push("Success story not found")
return return
} }
story.value = storyResponse story.value = storyResponse
const citResponse = await api.citizen.retrieve(story.value.citizenId, user) const citResponse = await api.citizen.retrieve(story.value.citizenId, user)
if (typeof citResponse === 'string') { if (typeof citResponse === "string") {
errors.push(citResponse) errors.push(citResponse)
} else if (typeof citResponse === 'undefined') { } else if (typeof citResponse === "undefined") {
errors.push('Citizen not found') errors.push("Citizen not found")
} else { } else {
citizenName.value = citResponse.realName || citResponse.displayName || citResponse.naUser citizenName.value = citName(citResponse)
}
} }
}
return { /** The HTML success story */
story, const successStory = computed(() => toHtml(story.value?.story ?? ""))
retrieveStory,
citizenName,
successStory: computed(() => marked(story.value?.story || '', markedOptions))
}
}
})
</script> </script>