Fixes
All checks were successful
ci / docker_image (push) Successful in 2m28s
ci / deploy (push) Successful in 24s

This commit is contained in:
Drake Marino 2025-06-19 17:14:18 -05:00
parent 8f41c04ca1
commit cbe002f4c4
17 changed files with 1052 additions and 109 deletions

640
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^5.2.9", "@sveltejs/adapter-node": "^5.2.9",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/vite": "^4.1.10",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"desm": "^1.3.1", "desm": "^1.3.1",
@ -40,7 +41,7 @@
"prettier-plugin-tailwindcss": "^0.6.9", "prettier-plugin-tailwindcss": "^0.6.9",
"svelte": "^5.0.0", "svelte": "^5.0.0",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"tailwindcss": "^3.4.9", "tailwindcss": "^3.4.17",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.0.0",
"vite": "^6.0.0" "vite": "^6.0.0"
@ -763,6 +764,27 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@isaacs/fs-minipass/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8", "version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@ -1329,6 +1351,360 @@
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
} }
}, },
"node_modules/@tailwindcss/node": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz",
"integrity": "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "1.30.1",
"magic-string": "^0.30.17",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.10"
}
},
"node_modules/@tailwindcss/node/node_modules/jiti": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
"integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
"license": "MIT"
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz",
"integrity": "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.4",
"tar": "^7.4.3"
},
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.10",
"@tailwindcss/oxide-darwin-arm64": "4.1.10",
"@tailwindcss/oxide-darwin-x64": "4.1.10",
"@tailwindcss/oxide-freebsd-x64": "4.1.10",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.10",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.10",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.10",
"@tailwindcss/oxide-linux-x64-musl": "4.1.10",
"@tailwindcss/oxide-wasm32-wasi": "4.1.10",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.10",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.10"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.10.tgz",
"integrity": "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.10.tgz",
"integrity": "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.10.tgz",
"integrity": "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.10.tgz",
"integrity": "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.10.tgz",
"integrity": "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.10.tgz",
"integrity": "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.10.tgz",
"integrity": "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.10.tgz",
"integrity": "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.10.tgz",
"integrity": "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.10.tgz",
"integrity": "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@emnapi/wasi-threads": "^1.0.2",
"@napi-rs/wasm-runtime": "^0.2.10",
"@tybys/wasm-util": "^0.9.0",
"tslib": "^2.8.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz",
"integrity": "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.10.tgz",
"integrity": "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide/node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/@tailwindcss/oxide/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/@tailwindcss/oxide/node_modules/minizlib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@tailwindcss/oxide/node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@tailwindcss/oxide/node_modules/tar": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@tailwindcss/oxide/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.10.tgz",
"integrity": "sha512-QWnD5HDY2IADv+vYR82lOhqOlS1jSCUUAmfem52cXAhRTKxpDh3ARX8TTXJTCCO7Rv7cD2Nlekabv02bwP3a2A==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.10",
"@tailwindcss/oxide": "4.1.10",
"tailwindcss": "4.1.10"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6"
}
},
"node_modules/@tailwindcss/vite/node_modules/tailwindcss": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
"integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
"license": "MIT"
},
"node_modules/@types/bcrypt": { "node_modules/@types/bcrypt": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
@ -2449,9 +2825,9 @@
} }
}, },
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.3", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -2543,6 +2919,19 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -3385,6 +3774,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/graphemer": { "node_modules/graphemer": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@ -3783,6 +4178,234 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lightningcss": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-darwin-arm64": "1.30.1",
"lightningcss-darwin-x64": "1.30.1",
"lightningcss-freebsd-x64": "1.30.1",
"lightningcss-linux-arm-gnueabihf": "1.30.1",
"lightningcss-linux-arm64-gnu": "1.30.1",
"lightningcss-linux-arm64-musl": "1.30.1",
"lightningcss-linux-x64-gnu": "1.30.1",
"lightningcss-linux-x64-musl": "1.30.1",
"lightningcss-win32-arm64-msvc": "1.30.1",
"lightningcss-win32-x64-msvc": "1.30.1"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lilconfig": { "node_modules/lilconfig": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@ -5823,6 +6446,15 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/tapable": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/tar": { "node_modules/tar": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",

View File

@ -31,7 +31,7 @@
"prettier-plugin-tailwindcss": "^0.6.9", "prettier-plugin-tailwindcss": "^0.6.9",
"svelte": "^5.0.0", "svelte": "^5.0.0",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"tailwindcss": "^3.4.9", "tailwindcss": "^3.4.17",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"typescript-eslint": "^8.0.0", "typescript-eslint": "^8.0.0",
"vite": "^6.0.0" "vite": "^6.0.0"
@ -39,6 +39,7 @@
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^5.2.9", "@sveltejs/adapter-node": "^5.2.9",
"@tailwindcss/forms": "^0.5.9", "@tailwindcss/forms": "^0.5.9",
"@tailwindcss/vite": "^4.1.10",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"desm": "^1.3.1", "desm": "^1.3.1",

View File

@ -20,12 +20,12 @@
[data-theme='dark'] { [data-theme='dark'] {
--text-color: #f4f4f4; --text-color: #f4f4f4;
--bg-color: #0c0c0c; --bg-color: #0c0c0c;
--hover-bg-color: #1a1c1c; --hover-bg-color: #1c1e1e;
--separator-line-color: #1a2029; --separator-line-color: #2e343d;
--low-emphasis-text-color: #999999; --low-emphasis-text-color: #999999;
--primary-color: #1F96F3; --primary-color: #1F96F3;
--dull-primary-color: #1569ab; --dull-primary-color: #1569ab;
--elevated-bg-color: #151516; --elevated-bg-color: #1a1a1b;
--bg-accent-color: #202020; --bg-accent-color: #202020;
--danger-color: #ff1d1f; --danger-color: #ff1d1f;
--hyperlink-color: #3b82f6; --hyperlink-color: #3b82f6;

View File

@ -40,31 +40,30 @@ export async function updateUser(user: User): Promise<number> {
} }
// Construct the SQL query // Construct the SQL query
const response = await sql`UPDATE users const response = await sql`
SET username = ${user.username}, UPDATE users
${ SET username = ${user.username},
user.perms !== undefined ${
? sql`perms user.perms !== undefined
= ? sql`perms = ${user.perms},`
${user.perms},` : sql``
: sql`` }
} ${
${ user.active !== undefined
user.active !== undefined ? sql`active = ${user.active},`
? sql`active : sql``
= }
${user.active},` ${
: sql`` password_hash !== null
} ${ ? sql`password_hash = ${password_hash},`
password_hash !== null : sql``
? sql`password_hash }
= email = ${user.email},
${password_hash},` phone = ${user.phone},
: sql`` full_name = ${user.fullName},
} company_code = ${user.companyCode}
email = ${user.email}, phone = ${user.phone}, full_name = ${user.fullName}, company_code = ${user.companyCode} WHERE id = ${user.id}
WHERE id = ${user.id} RETURNING id;`;
RETURNING id;`;
await saveAvatar(user); await saveAvatar(user);
@ -220,7 +219,8 @@ export async function getUserWithCompanyAndApplications(
application_data AS (SELECT id, application_data AS (SELECT id,
posting_id AS "postingId", posting_id AS "postingId",
(SELECT title FROM postings WHERE id = posting_id) AS "postingTitle", (SELECT title FROM postings WHERE id = posting_id) AS "postingTitle",
created_at AT TIME ZONE 'UTC' AS "createdAt" created_at AT TIME ZONE 'UTC' AS "createdAt",
candidate_statement AS "candidateStatement"
FROM applications FROM applications
WHERE "user_id" = ${id}) WHERE "user_id" = ${id})
SELECT (SELECT row_to_json(company_data) SELECT (SELECT row_to_json(company_data)
@ -236,7 +236,7 @@ export async function getUserWithCompanyAndApplications(
if (!data) { if (!data) {
error(404, 'User not found'); error(404, 'User not found');
} }
let user = data[0].user; const user = data[0].user;
if (data[0].company) { if (data[0].company) {
user.company = data[0].company; user.company = data[0].company;
} }
@ -246,7 +246,7 @@ export async function getUserWithCompanyAndApplications(
if (user.company) { if (user.company) {
user.company.createdAt = new Date(user.company.createdAt); user.company.createdAt = new Date(user.company.createdAt);
} }
let applications = data[0].applications; const applications = data[0].applications;
if (applications) { if (applications) {
applications.forEach((application: { createdAt: string | number | Date }) => { applications.forEach((application: { createdAt: string | number | Date }) => {
application.createdAt = new Date(application.createdAt); application.createdAt = new Date(application.createdAt);
@ -509,8 +509,8 @@ export async function getCompanyEmployers(
if (data[0].users) { if (data[0].users) {
data[0].users.forEach( data[0].users.forEach(
(user: { (user: {
company: { id: any }; company: { id: unknown };
companyId: any; companyId: unknown;
createdAt: string | number | Date; createdAt: string | number | Date;
lastSignIn: string | number | Date; lastSignIn: string | number | Date;
}) => { }) => {
@ -576,8 +576,8 @@ export async function getCompanyEmployersAndRequests(
if (data[0].employers) { if (data[0].employers) {
data[0].employers.forEach( data[0].employers.forEach(
(user: { (user: {
company: { id: any }; company: { id: unknown };
companyId: any; companyId: unknown;
createdAt: string | number | Date; createdAt: string | number | Date;
lastSignIn: string | number | Date; lastSignIn: string | number | Date;
}) => { }) => {
@ -593,8 +593,8 @@ export async function getCompanyEmployersAndRequests(
if (data[0].requests) { if (data[0].requests) {
data[0].requests.forEach( data[0].requests.forEach(
(user: { (user: {
company: { id: any }; company: { id: unknown };
companyId: any; companyId: unknown;
createdAt: string | number | Date; createdAt: string | number | Date;
lastSignIn: string | number | Date; lastSignIn: string | number | Date;
}) => { }) => {
@ -751,7 +751,7 @@ export async function getPostingWithCompanyUser(id: number): Promise<Posting> {
if (!data) { if (!data) {
error(404, 'Posting not found'); error(404, 'Posting not found');
} }
let posting = <Posting>data[0].posting; const posting = <Posting>data[0].posting;
posting.company = <Company>data[0].company; posting.company = <Company>data[0].company;
posting.employer = <User>data[0].user; posting.employer = <User>data[0].user;
@ -765,16 +765,107 @@ export async function getPostingWithCompanyUser(id: number): Promise<Posting> {
return posting; return posting;
} }
export async function createApplication(application: Application): Promise<number> {
const response = await sql` export async function getEditApplicationPageData(applicationId: number): Promise<{ posting: Posting; application: Application }> {
INSERT INTO applications (posting_id, user_id, candidate_statement, created_at) const data = await sql`
VALUES (${application.postingId}, ${application.userId}, ${application.candidateStatement}, NOW()) RETURNING id; WITH application_data AS (
SELECT id AS application_id,
candidate_statement,
posting_id
FROM applications
WHERE id = ${applicationId}
),
company_data AS (
SELECT id,
name,
description,
website,
created_at AS "createdAt"
FROM companies
WHERE id = (
SELECT company_id
FROM postings
WHERE id = (SELECT posting_id FROM application_data)
)
),
user_data AS (
SELECT username,
email,
phone,
full_name AS "fullName"
FROM users
WHERE company_id = (
SELECT company_id
FROM postings
WHERE id = (SELECT posting_id FROM application_data)
)
),
posting_data AS (
SELECT id,
title,
description,
employer_id AS "employerId",
address,
employment_type AS "employmentType",
wage,
link,
tag_ids AS "tagIds",
created_at AT TIME ZONE 'UTC' AS "createdAt",
updated_at AT TIME ZONE 'UTC' AS "updatedAt",
flyer_link AS "flyerLink"
FROM postings
WHERE id = (SELECT posting_id FROM application_data)
)
SELECT
(SELECT row_to_json(company_data) FROM company_data) AS company,
(SELECT row_to_json(user_data) FROM user_data) AS user,
(SELECT row_to_json(posting_data) FROM posting_data) AS posting,
(SELECT row_to_json(application_data) FROM application_data) AS application;
`; `;
sendEmployerNotificationEmail(application.postingId).catch((err) => { if (!data || !data[0]?.posting) {
console.error('Failed to send employer notification email: ', err); error(404, 'Posting not found');
}); }
return response[0].id;
const posting = <Posting>data[0].posting;
posting.company = <Company>data[0].company;
posting.employer = <User>data[0].user;
if (posting.createdAt) {
posting.createdAt = new Date(posting.createdAt);
}
if (posting.updatedAt) {
posting.updatedAt = new Date(posting.updatedAt);
}
const application = <Application>{
id: data[0].application.application_id,
candidateStatement: data[0].application.candidate_statement
};
return { posting, application };
}
export async function createApplication(application: Application): Promise<number> {
try {
const response = await sql`
INSERT INTO applications (posting_id, user_id, candidate_statement, created_at)
VALUES (${application.postingId}, ${application.userId}, ${application.candidateStatement}, NOW()) RETURNING id;
`;
sendEmployerNotificationEmail(application.postingId).catch((err) => {
console.error('Failed to send employer notification email: ', err);
});
return response[0].id;
} catch (err) {
if (err instanceof Error && err.message.includes('duplicate key value')) {
error(400, 'You have already applied for this job');
} else {
error(500, 'Internal server error while creating application');
}
}
} }
export async function deleteApplication(id: number): Promise<void> { export async function deleteApplication(id: number): Promise<void> {
@ -789,7 +880,6 @@ export async function deleteApplicationWithUser(
applicationId: number, applicationId: number,
userId: number userId: number
): Promise<void> { ): Promise<void> {
console.log(applicationId, userId);
await sql` await sql`
DELETE DELETE
FROM applications FROM applications
@ -798,6 +888,14 @@ export async function deleteApplicationWithUser(
`; `;
} }
export async function editApplication(application: Application): Promise<void> {
await sql`
UPDATE applications
SET candidate_statement = ${application.candidateStatement}
WHERE id = ${application.id};
`;
}
export async function getApplications(postingId: number): Promise<Application[]> { export async function getApplications(postingId: number): Promise<Application[]> {
const data = await sql` const data = await sql`
SELECT a.id, SELECT a.id,

View File

@ -13,8 +13,13 @@ export function setJWT(cookies: Cookies, user: User) {
if (process.env.JWT_SECRET === undefined) { if (process.env.JWT_SECRET === undefined) {
throw new Error('JWT_SECRET not defined'); throw new Error('JWT_SECRET not defined');
} }
if (process.env.BASE_URL === undefined) {
throw new Error('BASE_URL not defined');
}
const secure: boolean = process.env.BASE_URL?.includes('https://');
const maxAge = 60 * 60 * 24 * 30; // 30 days const maxAge = 60 * 60 * 24 * 30; // 30 days
const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' }); const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' });
cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false }); cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: secure, secure: secure });
} }

View File

@ -26,7 +26,7 @@ export function telFormatter(initial: string) {
return initial; return initial;
} }
export const getCookieValue = (name: String) => export const getCookieValue = (name: string) =>
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || ''; document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
export function updateUserState() { export function updateUserState() {

View File

@ -22,8 +22,13 @@
<p>This one is on our end...</p> <p>This one is on our end...</p>
<p>We are working to resolve this as fast as possible.</p> <p>We are working to resolve this as fast as possible.</p>
{/if} {/if}
{#if $page.status !== 404 && $page.status !== 403 && $page.status !== 401 && $page.status !== 500} {#if $page.status === 400}
<p>Invalid request!</p>
<p>Make sure you have correctly inputted all information.</p>
{/if}
{#if $page.status !== 404 && $page.status !== 403 && $page.status !== 401 && $page.status !== 500 && $page.status !== 400}
<p>An unexpected error has occurred.</p> <p>An unexpected error has occurred.</p>
<p>Please try again later</p> <p>Please try again later</p>
{/if} {/if}
<p class="mt-8">Error: {$page.error?.message}</p>
</div> </div>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import type { Company } from '$lib/types';
let applicationToDelete: number = $state(0); let applicationToDelete: number = $state(0);
let { data }: PageProps = $props(); let { data }: PageProps = $props();
@ -13,6 +14,25 @@
if (window.location.search.includes('refresh')) { if (window.location.search.includes('refresh')) {
location.replace(location.pathname); location.replace(location.pathname);
} }
const acc = document.getElementsByClassName('accordion');
for (let i = 0; i < acc.length; i++) {
acc[i].addEventListener('click', function (this: HTMLElement) {
this.classList.toggle('active');
this.children[1].innerHTML = this.classList.contains('active')
? 'arrow_drop_up'
: 'arrow_drop_down';
/* Toggle between hiding and showing the active panel */
let panel = this.nextElementSibling as HTMLElement;
if (panel.style.display === 'flex') {
panel.style.display = 'none';
} else {
panel.style.display = 'flex';
}
});
}
}); });
const dateFormatOptions: Intl.DateTimeFormatOptions = { const dateFormatOptions: Intl.DateTimeFormatOptions = {
@ -26,11 +46,16 @@
`https://ui-avatars.com/api/?background=random&format=svg&name=${data.user.fullName ? encodeURIComponent(data.user.fullName) : encodeURIComponent(data.user.username)}`; `https://ui-avatars.com/api/?background=random&format=svg&name=${data.user.fullName ? encodeURIComponent(data.user.fullName) : encodeURIComponent(data.user.username)}`;
} }
function logoFallback(e: Event) { function logoFallback(e: Event, company: Company) {
(e.target as HTMLImageElement).src = (e.target as HTMLImageElement).src =
`https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(data.user.company!.name!)}`; `https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(company.name!)}`;
} }
// function logoFallback(e: Event) {
// (e.target as HTMLImageElement).src =
// `https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(data.user.company!.name!)}`;
// }
function signOut() { function signOut() {
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
window.location.href = '/signin'; window.location.href = '/signin';
@ -198,7 +223,7 @@
class="mb-2 inline-block rounded" class="mb-2 inline-block rounded"
src="/uploads/logos/{data.user.company.id}.jpg?timestamp=${Date.now()}" src="/uploads/logos/{data.user.company.id}.jpg?timestamp=${Date.now()}"
alt="Company Logo" alt="Company Logo"
onerror={logoFallback} onerror={(e) => logoFallback(e, data.user.company!)}
height="32" height="32"
width="32" width="32"
/> />
@ -236,28 +261,43 @@
<div class="elevated separator-borders m-2 inline-block h-min w-full rounded"> <div class="elevated separator-borders m-2 inline-block h-min w-full rounded">
<div class="p-3 font-semibold">Pending applications</div> <div class="p-3 font-semibold">Pending applications</div>
{#each data.applications as application} {#each data.applications as application}
<div class="top-border flex justify-between p-3"> <button class="top-border flex justify-between p-2 accordion">
<div class="inline-block"> <div class="inline-block">
<div class="font-semibold"> <div class="font-semibold text-left">
Applied to: <a Applied to: <a
class="hover-hyperlink font-normal" class="hover-hyperlink font-normal"
href="/postings/{application.postingId}">{application.postingTitle}</a href="/postings/{application.postingId}">{application.postingTitle}</a
> >
</div> </div>
<div class="font-semibold"> <div class="font-semibold text-left">
Applied on: <span class="font-normal" Applied on: <span class="font-normal"
>{application.createdAt.toLocaleDateString('en-US', dateFormatOptions)}</span >{application.createdAt.toLocaleDateString('en-US', dateFormatOptions)}</span
> >
</div> </div>
</div> </div>
<button <div class="material-symbols-outlined align-top pt-3 pr-3">arrow_drop_down</div>
class="material-symbols-outlined danger-color inline-block" </button>
onclick={() => { <div class="panel hidden p-2 justify-between">
<div class="inline-block">
<!-- <h2>Candidate Statement</h2>-->
<h3 class="low-emphasis-text">
Why do you believe you are the best fit for this role?
</h3>
<p class="whitespace-pre-wrap">{application.candidateStatement}</p>
</div>
<div class="mt-3 mr-3">
<a href="account/editapplication?id={application.id}" class="mr-3 material-symbols-outlined hyperlink-color">edit</a>
<button
class="material-symbols-outlined danger-color inline-block"
onclick={() => {
applicationToDelete = application.id; applicationToDelete = application.id;
openConfirm(); openConfirm();
}} }}
>delete >delete
</button> </button>
</div>
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -0,0 +1,47 @@
import { type Actions, error, fail, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { editApplication, getEditApplicationPageData } from '$lib/db/index.server';
import { getUserId, getUserPerms } from '$lib/index.server';
import { PERMISSIONS } from '$lib/consts';
import type { Application } from '$lib/types';
export const load: PageServerLoad = async ({ url, cookies }) => {
if (!url.searchParams.has('id')) {
error(400, 'Missing required parameter: id');
}
// Permission check (apply perm)
const id = parseInt(url.searchParams.get('id')!);
const perms = getUserPerms(cookies);
if (perms >= 0 && (perms & PERMISSIONS.APPLY_FOR_JOBS) > 0) {
return await getEditApplicationPageData(id);
}
error(403, 'Unauthorized');
};
export const actions: Actions = {
// Application submission
submit: async ({ request, cookies, params, url }) => {
// Permission check (apply perm)
if (!((getUserPerms(cookies) & PERMISSIONS.APPLY_FOR_JOBS) > 0)) {
return fail(403, { errorMessage: 'Unauthorized' });
}
const data = await request.formData();
const candidateStatement = data.get('candidateStatement')?.toString().trim();
// Statement data validation
if (!candidateStatement || candidateStatement === '') {
return fail(400, { errorMessage: 'Candidate statement is required' });
}
// Push to DB and redirect
if (!url.searchParams.has('id')) {
error(400, 'Missing required parameter: id');
}
const id: number = parseInt(url.searchParams.get('id')!);
await editApplication(<Application>{ id, candidateStatement });
redirect(301, `/postings`);
}
};

View File

@ -0,0 +1,117 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageProps } from './$types';
import { employmentTypeDisplayName } from '$lib/shared.svelte';
import type { Posting } from '$lib/types';
const dateFormatOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'short',
day: 'numeric'
};
function logoFallback(e: Event, posting: Posting) {
(e.target as HTMLImageElement).src =
`https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(posting.company.name || 'COMPANY')}`;
}
let { data, form }: PageProps = $props();
</script>
<div class="base-container">
<div class="content flex">
<div class="elevated separator-borders ml-4 mt-4 inline-block h-min w-1/2 rounded p-4">
<div class="bottom-border elevated-bg flex justify-between pb-2">
<div class="inline-block">
<img
alt="Company Logo"
class="inline-block rounded"
height="64"
onerror={(e) => logoFallback(e, data.posting)}
src="/uploads/logos/{data.posting?.company.id}.jpg"
width="64"
/>
<div class="inline-block pl-2 align-top">
<h1>{data.posting.title}</h1>
<h2>Company: {data.posting.company.name}</h2>
</div>
</div>
</div>
<div class="scrollbar-on-elevated details-height overflow-y-scroll">
<h2 class="pt-2 font-semibold">Contact</h2>
<p>{data.posting.employer?.fullName} ({data.posting.employer?.username})</p>
<a class="hover-hyperlink" href="mailto:{data.posting.employer?.email}"
>{data.posting.employer?.email}</a
>
<a class="hover-hyperlink" href="tel:{data.posting.employer?.phone}"
>{data.posting.employer?.phone}</a
>
<h2 class="pt-2 font-semibold">Details</h2>
{#if data.posting.employmentType}
<p>{employmentTypeDisplayName(data.posting.employmentType)}</p>
{/if}
{#if data.posting.address}
<a
href="https://www.google.com/maps/search/?api=1&query={data.posting.address}"
class="block w-max"
>Address: <span class="hover-hyperlink">{data.posting.address}</span></a
>
{/if}
{#if data.posting.wage}
<p>Wage: {data.posting.wage}</p>
{/if}
{#if data.posting.createdAt}
<p>Posted: {data.posting.createdAt.toLocaleDateString('en-US', dateFormatOptions)}</p>
{/if}
{#if data.posting.link}
<a href={data.posting.link} class="block w-max"
>More information: <span class="hyperlink-color hyperlink-underline"
>{data.posting.link}</span
></a
>
{/if}
{#if data.posting.flyerLink}
<a href={data.posting.flyerLink} class="block w-max"
>Flyer: <span class="hyperlink-color hyperlink-underline">{data.posting.flyerLink}</span
></a
>
{/if}
<h2 class="pt-2 font-semibold">Job Description</h2>
<p class="whitespace-pre-wrap">{data.posting.description}</p>
</div>
</div>
<div class="elevated separator-borders m-4 inline-block h-min w-1/2 rounded">
<div class="bottom-border flex place-content-between">
<div class="p-3 font-semibold">Edit Application</div>
</div>
<form autocomplete="off" class="px-4" method="POST" use:enhance>
<div class="mt-4 text-sm font-semibold">
Why do you believe you are the best fit for this role? <span class="text-red-500">*</span>
<textarea
class="w-full rounded font-normal"
id="candidateStatement"
name="candidateStatement"
placeholder="Answer here"
required
rows="4"
>{data.application.candidateStatement}</textarea>
</div>
<p>
Your account information and résumé (if supplied) will be submitted along with this
application. If there is any other information you would like the employer to know, please
add it in the box above.
</p>
{#if form?.errorMessage}
<div class="mb-2 text-red-500">{form.errorMessage}</div>
{/if}
<button
class="dull-primary-bg-color mb-4 mt-6 rounded px-2 py-1"
formaction="?/submit&id={data.application.id}"
type="submit"
>Save application
</button>
</form>
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@
let passwordVisible = $state(false); let passwordVisible = $state(false);
function showPassword() { function showPassword() {
const password = document.querySelector('input[name="password"]'); const password = document.querySelector('input[name="newPassword"]');
if (password) { if (password) {
if (password.getAttribute('type') === 'password') { if (password.getAttribute('type') === 'password') {
password.setAttribute('type', 'text'); password.setAttribute('type', 'text');
@ -122,8 +122,8 @@
New password (optional) New password (optional)
<input <input
class="w-full rounded font-normal" class="w-full rounded font-normal"
id="password" id="newPassword"
name="password" name="newPassword"
placeholder="New password" placeholder="New password"
type="password" type="password"
/> />

View File

@ -9,7 +9,7 @@
let passwordVisible = $state(false); let passwordVisible = $state(false);
function showPassword() { function showPassword() {
const password = document.querySelector('input[name="password"]'); const password = document.querySelector('input[name="newPassword"]');
if (password) { if (password) {
if (password.getAttribute('type') === 'password') { if (password.getAttribute('type') === 'password') {
password.setAttribute('type', 'text'); password.setAttribute('type', 'text');
@ -102,8 +102,8 @@
Password <span class="text-red-500">*</span> Password <span class="text-red-500">*</span>
<input <input
class="w-full rounded font-normal" class="w-full rounded font-normal"
id="password" id="newPassword"
name="password" name="newPassword"
placeholder="Password" placeholder="Password"
required required
type="password" type="password"

View File

@ -112,7 +112,7 @@
{#if userState.perms >= 0 && ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0 || ((userState.perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && userState.companyId === details.company.id))} {#if userState.perms >= 0 && ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0 || ((userState.perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && userState.companyId === details.company.id))}
<a <a
class="dull-primary-bg-color inline-block h-min rounded-md px-2.5 py-1 align-top" class="dull-primary-bg-color inline-block h-min rounded-md px-2.5 py-1 align-top"
href="/postings/{details.id}/manage">Manage posting</a href="/postings/{details.id}/manage/applications/">Manage posting</a
> >
{:else if (userState.perms & PERMISSIONS.APPLY_FOR_JOBS) > 0} {:else if (userState.perms & PERMISSIONS.APPLY_FOR_JOBS) > 0}
<a <a

View File

@ -41,7 +41,7 @@
{#if userState.perms >= 0 && ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0 || ((userState.perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && userState.companyId === data.posting.company.id))} {#if userState.perms >= 0 && ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0 || ((userState.perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && userState.companyId === data.posting.company.id))}
<a <a
class="dull-primary-bg-color inline-block h-min rounded-md px-2.5 py-1 align-top" class="dull-primary-bg-color inline-block h-min rounded-md px-2.5 py-1 align-top"
href="/postings/{data.posting.id}/manage">Manage posting</a href="/postings/{data.posting.id}/manage/applications/">Manage posting</a
> >
{:else if (userState.perms & PERMISSIONS.APPLY_FOR_JOBS) > 0} {:else if (userState.perms & PERMISSIONS.APPLY_FOR_JOBS) > 0}
<a <a

View File

@ -1,4 +1,4 @@
<script> <script>editApplication
import { onMount } from 'svelte'; import { onMount } from 'svelte';
onMount(() => { onMount(() => {

View File

@ -26,16 +26,16 @@
/* Toggle between hiding and showing the active panel */ /* Toggle between hiding and showing the active panel */
let panel = this.nextElementSibling as HTMLElement; let panel = this.nextElementSibling as HTMLElement;
if (panel.style.display === 'block') { if (panel.style.display === 'flex') {
panel.style.display = 'none'; panel.style.display = 'none';
} else { } else {
panel.style.display = 'block'; panel.style.display = 'flex';
} }
}); });
} }
}); });
let { data, form }: PageProps = $props(); let { data }: PageProps = $props();
</script> </script>
<div class="content"> <div class="content">
@ -52,7 +52,7 @@
<img <img
class="m-2 inline-block rounded" class="m-2 inline-block rounded"
src="/uploads/avatars/{application.user?.id || 'default'}.svg" src="/uploads/avatars/{application.user?.id || 'default'}.svg"
alt="Company Logo" alt="User Avatar"
height="32" height="32"
width="32" width="32"
onerror={(e) => logoFallback(e, application)} onerror={(e) => logoFallback(e, application)}
@ -70,36 +70,34 @@
</button> </button>
<div class="panel hidden p-2"> <div class="panel hidden p-2">
<div class="flex justify-between"> <div class="flex justify-between">
<div class="inline-block"> <div class="inline-block pr-4 min-w-max">
<div class="inline-block pr-4 align-top"> {#if application.user?.email}
{#if application.user?.email} <p>Email: {application.user.email}</p>
<p>Email: {application.user.email}</p> {/if}
{/if} {#if application.user?.phone}
{#if application.user?.phone} <p>Phone: {application.user.phone}</p>
<p>Phone: {application.user.phone}</p> {/if}
{/if} {#if application.createdAt}
{#if application.createdAt} <p>
<p> Applied: {application.createdAt.toLocaleDateString('en-US', dateFormatOptions)}
Applied: {application.createdAt.toLocaleDateString('en-US', dateFormatOptions)} </p>
</p> {/if}
{/if} {#if application.user?.resume}
{#if application.user?.resume} <a
<a class="hyperlink-underline hyperlink-color"
class="hyperlink-underline hyperlink-color" href="/uploads/resumes/{application.user.id}.pdf"
href="/uploads/resumes/{application.user.id}.pdf" target="_blank"
target="_blank" >
> Résumé
Résumé </a>
</a> {/if}
{/if} </div>
</div> <div class="left-border inline-block pl-4">
<div class="left-border inline-block pl-4"> <h2>Candidate Statement</h2>
<h2>Candidate Statement</h2> <h3 class="low-emphasis-text">
<h3 class="low-emphasis-text"> Why do you believe you are the best fit for this role?
Why do you believe you are the best fit for this role? </h3>
</h3> <p class="whitespace-pre-wrap">{application.candidateStatement}</p>
<p class="whitespace-pre-wrap">{application.candidateStatement}</p>
</div>
</div> </div>
<form method="POST"> <form method="POST">
<button <button

View File

@ -149,7 +149,7 @@
<div class="mt-4 flex justify-between"> <div class="mt-4 flex justify-between">
<button <button
class="danger-bg-color rounded px-2 py-1" class="danger-bg-color rounded px-2 py-1"
formaction="?/delete&id={data.posting.id}" formaction="?/delete"
type="submit" type="submit"
>Delete posting >Delete posting
</button> </button>