dev
This commit is contained in:
parent
be83b7570d
commit
fa14fe0496
382
package-lock.json
generated
382
package-lock.json
generated
@ -16,6 +16,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"postgres": "^3.4.5",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"vitest": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -592,9 +593,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz",
|
||||
"integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==",
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
|
||||
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -970,9 +971,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz",
|
||||
"integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz",
|
||||
"integrity": "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -983,9 +984,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz",
|
||||
"integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.1.tgz",
|
||||
"integrity": "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -996,9 +997,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz",
|
||||
"integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.1.tgz",
|
||||
"integrity": "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1009,9 +1010,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz",
|
||||
"integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.1.tgz",
|
||||
"integrity": "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1022,9 +1023,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz",
|
||||
"integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.1.tgz",
|
||||
"integrity": "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1035,9 +1036,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz",
|
||||
"integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.1.tgz",
|
||||
"integrity": "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1048,9 +1049,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz",
|
||||
"integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.1.tgz",
|
||||
"integrity": "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -1061,9 +1062,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz",
|
||||
"integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.1.tgz",
|
||||
"integrity": "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -1074,9 +1075,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz",
|
||||
"integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.1.tgz",
|
||||
"integrity": "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1087,9 +1088,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz",
|
||||
"integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.1.tgz",
|
||||
"integrity": "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1100,9 +1101,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz",
|
||||
"integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.1.tgz",
|
||||
"integrity": "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@ -1113,9 +1114,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz",
|
||||
"integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.1.tgz",
|
||||
"integrity": "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -1126,9 +1127,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz",
|
||||
"integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.1.tgz",
|
||||
"integrity": "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -1139,9 +1140,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz",
|
||||
"integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.1.tgz",
|
||||
"integrity": "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -1152,9 +1153,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz",
|
||||
"integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.1.tgz",
|
||||
"integrity": "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1165,9 +1166,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz",
|
||||
"integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.1.tgz",
|
||||
"integrity": "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1178,9 +1179,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz",
|
||||
"integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.1.tgz",
|
||||
"integrity": "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -1191,9 +1192,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz",
|
||||
"integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.1.tgz",
|
||||
"integrity": "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -1204,9 +1205,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz",
|
||||
"integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.1.tgz",
|
||||
"integrity": "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -1354,19 +1355,27 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsonwebtoken": {
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz",
|
||||
"integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==",
|
||||
"version": "9.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.8.tgz",
|
||||
"integrity": "sha512-7fx54m60nLFUVYlxAB1xpe9CBWX2vSrk50Y6ogRJ1v5xxtba7qXTg5BgYDN5dq+yuQQ9HaVlHJyAAt1/mxryFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ms": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz",
|
||||
"integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==",
|
||||
"version": "22.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz",
|
||||
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1391,17 +1400,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz",
|
||||
"integrity": "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==",
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz",
|
||||
"integrity": "sha512-4Uta6REnz/xEJMvwf72wdUnC3rr4jAQf5jnTkeRQ9b6soxLxhDEbS/pfMPoJLDfFPNVRdryqWUIV/2GZzDJFZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.21.0",
|
||||
"@typescript-eslint/type-utils": "8.21.0",
|
||||
"@typescript-eslint/utils": "8.21.0",
|
||||
"@typescript-eslint/visitor-keys": "8.21.0",
|
||||
"@typescript-eslint/scope-manager": "8.22.0",
|
||||
"@typescript-eslint/type-utils": "8.22.0",
|
||||
"@typescript-eslint/utils": "8.22.0",
|
||||
"@typescript-eslint/visitor-keys": "8.22.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@ -1421,16 +1430,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.21.0.tgz",
|
||||
"integrity": "sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==",
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.22.0.tgz",
|
||||
"integrity": "sha512-MqtmbdNEdoNxTPzpWiWnqNac54h8JDAmkWtJExBVVnSrSmi9z+sZUt0LfKqk9rjqmKOIeRhO4fHHJ1nQIjduIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.21.0",
|
||||
"@typescript-eslint/types": "8.21.0",
|
||||
"@typescript-eslint/typescript-estree": "8.21.0",
|
||||
"@typescript-eslint/visitor-keys": "8.21.0",
|
||||
"@typescript-eslint/scope-manager": "8.22.0",
|
||||
"@typescript-eslint/types": "8.22.0",
|
||||
"@typescript-eslint/typescript-estree": "8.22.0",
|
||||
"@typescript-eslint/visitor-keys": "8.22.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@ -1446,14 +1455,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz",
|
||||
"integrity": "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==",
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz",
|
||||
"integrity": "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.21.0",
|
||||
"@typescript-eslint/visitor-keys": "8.21.0"
|
||||
"@typescript-eslint/types": "8.22.0",
|
||||
"@typescript-eslint/visitor-keys": "8.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@ -1464,14 +1473,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz",
|
||||
"integrity": "sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==",
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.22.0.tgz",
|
||||
"integrity": "sha512-NzE3aB62fDEaGjaAYZE4LH7I1MUwHooQ98Byq0G0y3kkibPJQIXVUspzlFOmOfHhiDLwKzMlWxaNv+/qcZurJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.21.0",
|
||||
"@typescript-eslint/utils": "8.21.0",
|
||||
"@typescript-eslint/typescript-estree": "8.22.0",
|
||||
"@typescript-eslint/utils": "8.22.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.0.0"
|
||||
},
|
||||
@ -1488,9 +1497,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.21.0.tgz",
|
||||
"integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==",
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz",
|
||||
"integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -1502,14 +1511,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz",
|
||||
"integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==",
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz",
|
||||
"integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.21.0",
|
||||
"@typescript-eslint/visitor-keys": "8.21.0",
|
||||
"@typescript-eslint/types": "8.22.0",
|
||||
"@typescript-eslint/visitor-keys": "8.22.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@ -1555,16 +1564,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.21.0.tgz",
|
||||
"integrity": "sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==",
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.22.0.tgz",
|
||||
"integrity": "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.21.0",
|
||||
"@typescript-eslint/types": "8.21.0",
|
||||
"@typescript-eslint/typescript-estree": "8.21.0"
|
||||
"@typescript-eslint/scope-manager": "8.22.0",
|
||||
"@typescript-eslint/types": "8.22.0",
|
||||
"@typescript-eslint/typescript-estree": "8.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@ -1579,13 +1588,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz",
|
||||
"integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==",
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz",
|
||||
"integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.21.0",
|
||||
"@typescript-eslint/types": "8.22.0",
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -2022,9 +2031,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001695",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz",
|
||||
"integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==",
|
||||
"version": "1.0.30001696",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz",
|
||||
"integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -2332,9 +2341,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.87",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.87.tgz",
|
||||
"integrity": "sha512-mPFwmEWmRivw2F8x3w3l2m6htAUN97Gy0kwpO++2m9iT1Gt8RCFVUfv9U/sIbHJ6rY4P6/ooqFL/eL7ock+pPg==",
|
||||
"version": "1.5.88",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz",
|
||||
"integrity": "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@ -2412,9 +2421,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz",
|
||||
"integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==",
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
|
||||
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2423,7 +2432,7 @@
|
||||
"@eslint/config-array": "^0.19.0",
|
||||
"@eslint/core": "^0.10.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "9.18.0",
|
||||
"@eslint/js": "9.19.0",
|
||||
"@eslint/plugin-kit": "^0.2.5",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@ -3323,7 +3332,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@ -3407,9 +3416,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
|
||||
"integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
|
||||
"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
@ -3978,7 +3987,7 @@
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
|
||||
"integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lilconfig": "^2.0.5",
|
||||
@ -4008,7 +4017,7 @@
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@ -4355,9 +4364,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.31.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz",
|
||||
"integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==",
|
||||
"version": "4.32.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.1.tgz",
|
||||
"integrity": "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.6"
|
||||
@ -4370,25 +4379,25 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.31.0",
|
||||
"@rollup/rollup-android-arm64": "4.31.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.31.0",
|
||||
"@rollup/rollup-darwin-x64": "4.31.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.31.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.31.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.31.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.31.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.31.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.31.0",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.31.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.31.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.31.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.31.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.31.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.31.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.31.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.31.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.31.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.32.1",
|
||||
"@rollup/rollup-android-arm64": "4.32.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.32.1",
|
||||
"@rollup/rollup-darwin-x64": "4.32.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.32.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.32.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.32.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.32.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.32.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.32.1",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.32.1",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.32.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.32.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.32.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.32.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.32.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.32.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.32.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.32.1",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@ -4448,9 +4457,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"version": "7.7.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
|
||||
"integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@ -4716,9 +4725,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.19.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.2.tgz",
|
||||
"integrity": "sha512-Ww1uLgdX5MdQrAO5zfU1dWUh6zqiPR6uIbwqm8a+4eQ+tNEYHRPgypvKKfHh9lmTkmJ30PWZ2O5qX8aS+PblRQ==",
|
||||
"version": "5.19.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.5.tgz",
|
||||
"integrity": "sha512-vVAntseegJX80sgbY8CxQISSE/VoDSfP7VZHoQaf2+z+2XOPOz/N+k455HJmO9O0g8oxTtuE0TBhC/5LAP4lPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
@ -4840,6 +4849,61 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-preprocess": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz",
|
||||
"integrity": "sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.10.2",
|
||||
"coffeescript": "^2.5.1",
|
||||
"less": "^3.11.3 || ^4.0.0",
|
||||
"postcss": "^7 || ^8",
|
||||
"postcss-load-config": ">=3",
|
||||
"pug": "^3.0.0",
|
||||
"sass": "^1.26.8",
|
||||
"stylus": ">=0.55",
|
||||
"sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.100 || ^5.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/core": {
|
||||
"optional": true
|
||||
},
|
||||
"coffeescript": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"postcss": {
|
||||
"optional": true
|
||||
},
|
||||
"postcss-load-config": {
|
||||
"optional": true
|
||||
},
|
||||
"pug": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte/node_modules/is-reference": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
@ -5133,7 +5197,7 @@
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@ -5144,15 +5208,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.21.0.tgz",
|
||||
"integrity": "sha512-txEKYY4XMKwPXxNkN8+AxAdX6iIJAPiJbHE/FpQccs/sxw8Lf26kqwC3cn0xkHlW8kEbLhkhCsjWuMveaY9Rxw==",
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.22.0.tgz",
|
||||
"integrity": "sha512-Y2rj210FW1Wb6TWXzQc5+P+EWI9/zdS57hLEc0gnyuvdzWo8+Y8brKlbj0muejonhMI/xAZCnZZwjbIfv1CkOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.21.0",
|
||||
"@typescript-eslint/parser": "8.21.0",
|
||||
"@typescript-eslint/utils": "8.21.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.22.0",
|
||||
"@typescript-eslint/parser": "8.22.0",
|
||||
"@typescript-eslint/utils": "8.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
@ -44,6 +44,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"postgres": "^3.4.5",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"vitest": "^2.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
125
src/app.css
125
src/app.css
@ -14,6 +14,7 @@
|
||||
--elevated-bg-color: #ffffff;
|
||||
--bg-accent-color: #f4f4f4;
|
||||
--danger-color: #ff2d2f;
|
||||
--hyperlink-color: #3b82f6;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
@ -27,6 +28,7 @@
|
||||
--elevated-bg-color: #101011;
|
||||
--bg-accent-color: #202020;
|
||||
--danger-color: #ff1d1f;
|
||||
--hyperlink-color: #3b82f6;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -71,6 +73,14 @@ h1 {
|
||||
border-bottom: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
.left-border {
|
||||
border-left: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
.right-border {
|
||||
border-right: 1px solid var(--separator-line-color);
|
||||
}
|
||||
|
||||
.icon-16 {
|
||||
font-size: 16px !important;
|
||||
font-variation-settings:
|
||||
@ -89,21 +99,24 @@ h1 {
|
||||
'opsz' 20
|
||||
}
|
||||
|
||||
input[type='search'], input[type='text'], input[type='password'], input[type='email'], input[type='tel'] {
|
||||
input[type='search'], input[type='text'], input[type='password'], input[type='email'], input[type='tel'], input[type='number'], textarea, select {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--separator-line-color);
|
||||
caret-color: var(--text-color);
|
||||
}
|
||||
|
||||
input[type='search']:focus, input[type='text']:focus, input[type='password']:focus, input[type='email']:focus, input[type='tel']:focus {
|
||||
input[type='search']:focus, input[type='text']:focus, input[type='password']:focus, input[type='email']:focus, input[type='tel']:focus, input[type='number']:focus, textarea:focus, select:focus {
|
||||
outline: 0 solid var(--text-color);
|
||||
border: 1px solid var(--primary-color);
|
||||
box-shadow: 0 0 0 0 var(--primary-color);
|
||||
caret-color: var(--text-color);
|
||||
}
|
||||
|
||||
input[type='search']:-webkit-autofill, input[type='text']:-webkit-autofill, input[type='password']:-webkit-autofill, input[type='email']:-webkit-autofill, input[type='tel']:-webkit-autofill {
|
||||
input[type='search']:-webkit-autofill, input[type='text']:-webkit-autofill, input[type='password']:-webkit-autofill, input[type='email']:-webkit-autofill, input[type='tel']:-webkit-autofill, input[type='number']:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill {
|
||||
-webkit-text-fill-color: var(--text-color) !important;
|
||||
background-clip: text !important;
|
||||
caret-color: var(--text-color);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@ -155,6 +168,12 @@ input[type='search']:-webkit-autofill, input[type='text']:-webkit-autofill, inpu
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.base-container-small {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.signin-container {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
@ -167,6 +186,7 @@ input[type='search']:-webkit-autofill, input[type='text']:-webkit-autofill, inpu
|
||||
@apply px-6;
|
||||
}
|
||||
|
||||
|
||||
.search-cancel::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
background-color: var(--text-color);
|
||||
@ -200,10 +220,18 @@ th.left, td.left {
|
||||
@apply shadow-lg
|
||||
}
|
||||
|
||||
.elevated-bg {
|
||||
background-color: var(--elevated-bg-color);
|
||||
}
|
||||
|
||||
.primary-bg-color {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.accent-bg-color {
|
||||
background-color: var(--bg-accent-color);
|
||||
}
|
||||
|
||||
.dull-primary-bg-color {
|
||||
background-color: var(--dull-primary-color);
|
||||
}
|
||||
@ -241,6 +269,10 @@ input[type='checkbox']:focus {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.danger-color {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.danger-border-color {
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
@ -259,6 +291,18 @@ input[type='checkbox']:focus {
|
||||
background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
|
||||
}
|
||||
|
||||
.modal-always-display {
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1; /* Sit on top */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%; /* Full width */
|
||||
height: 100%; /* Full height */
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgb(0,0,0); /* Fallback color */
|
||||
background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
|
||||
}
|
||||
|
||||
/* Modal Content/Box */
|
||||
.modal-content {
|
||||
background-color: var(--bg-color);
|
||||
@ -269,33 +313,18 @@ input[type='checkbox']:focus {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* The Close Button */
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--text-color);
|
||||
@apply text-2xl
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
/*.tooltip {*/
|
||||
/* position: relative;*/
|
||||
/* display: inline-block;*/
|
||||
/*}*/
|
||||
|
||||
/* Tooltip text */
|
||||
.tooltip .tooltip-text {
|
||||
.tooltip-text {
|
||||
visibility: hidden;
|
||||
background-color: var(--bg-accent-color);
|
||||
color: var(--text-color);
|
||||
@ -306,16 +335,16 @@ h2 {
|
||||
/* Position the tooltip text - see examples below! */
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100px;
|
||||
width: 120px;
|
||||
bottom: 120%;
|
||||
left: 50%;
|
||||
margin-left: -50px; /* Use half of the width (120/2 = 60), to center the tooltip */
|
||||
margin-left: -60px; /* Use half of the width (120/2 = 60), to center the tooltip */
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0s linear 0.5s;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-text::after {
|
||||
.tooltip-text::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 100%; /* At the bottom of the tooltip */
|
||||
@ -338,8 +367,16 @@ h2 {
|
||||
}
|
||||
|
||||
.hover-hyperlink:hover {
|
||||
color: var(--dull-primary-color);
|
||||
text-decoration: underline var(--dull-primary-color);;
|
||||
color: var(--hyperlink-color);
|
||||
text-decoration: underline var(--hyperlink-color);
|
||||
}
|
||||
|
||||
.hyperlink-color {
|
||||
color: var(--hyperlink-color);
|
||||
}
|
||||
|
||||
.hyperlink-underline {
|
||||
text-decoration: underline var(--hyperlink-color);
|
||||
}
|
||||
|
||||
.max-char-length {
|
||||
@ -347,3 +384,35 @@ h2 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-accent-color);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--separator-line-color);
|
||||
}
|
||||
|
||||
.scrollbar-on-elevated::-webkit-scrollbar-track {
|
||||
background: var(--elevated-bg-color);
|
||||
}
|
||||
|
||||
.-webkit-scrollbar-thumb::-webkit-scrollbar-track {
|
||||
background: var(--elevated-bg-color);
|
||||
}
|
||||
|
||||
.details-height {
|
||||
max-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.top-with-navbar {
|
||||
top: 72px;
|
||||
}
|
||||
|
||||
@ -2,6 +2,14 @@ import bcrypt from 'bcrypt';
|
||||
import sql from '$lib/db/db.server';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { saveAvatar } from '$lib/index.server';
|
||||
import {
|
||||
EmploymentType,
|
||||
type User,
|
||||
type Company,
|
||||
type Tag,
|
||||
type Posting,
|
||||
type Application
|
||||
} from '$lib/types';
|
||||
|
||||
export async function createUser(user: User): Promise<number> {
|
||||
const password_hash: string = await bcrypt.hash(user.password!, 12);
|
||||
@ -48,7 +56,7 @@ export async function updateUser(user: User): Promise<number> {
|
||||
|
||||
export async function checkUserCreds(username: string, password: string): Promise<User | null> {
|
||||
const [user] = await sql`
|
||||
SELECT id, password_hash, perms, active
|
||||
SELECT id, username, password_hash, perms, active, company_id AS "companyId"
|
||||
FROM users
|
||||
WHERE username = ${username}
|
||||
`;
|
||||
@ -57,7 +65,8 @@ export async function checkUserCreds(username: string, password: string): Promis
|
||||
return null;
|
||||
}
|
||||
if (await bcrypt.compare(password, user.password_hash)) {
|
||||
return <User>{ id: user.id, perms: user.perms, active: user.active };
|
||||
delete user.password_hash;
|
||||
return <User>user;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -87,6 +96,19 @@ export async function getUsers(searchQuery: string | null = null): Promise<User[
|
||||
return <User[]>(<unknown>users);
|
||||
}
|
||||
|
||||
export async function getCompanies(searchQuery: string | null = null): Promise<Company[]> {
|
||||
return sql<Company[]>`
|
||||
SELECT id,
|
||||
name,
|
||||
description,
|
||||
website,
|
||||
company_code AS "companyCode",
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt"
|
||||
FROM companies
|
||||
WHERE name ILIKE ${searchQuery ? `%${searchQuery}%` : '%'};
|
||||
`;
|
||||
}
|
||||
|
||||
// should require MANAGE_USERS permission
|
||||
export async function getUser(id: number): Promise<User> {
|
||||
const [user] = await sql`
|
||||
@ -152,9 +174,84 @@ export async function getUserWithCompany(id: number): Promise<User> {
|
||||
return <User>user;
|
||||
}
|
||||
|
||||
export async function getUserWithCompanyAndApplications(
|
||||
id: number
|
||||
): Promise<{ user: User; applications: Application[] }> {
|
||||
const data = await sql`
|
||||
WITH company_data AS (
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
website,
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt"
|
||||
FROM companies
|
||||
WHERE id = (SELECT company_id FROM users WHERE id = ${id})
|
||||
),
|
||||
user_data AS (
|
||||
SELECT
|
||||
username,
|
||||
perms,
|
||||
email,
|
||||
phone,
|
||||
full_name AS "fullName",
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
last_signin AT TIME ZONE 'UTC' AS "lastSignIn",
|
||||
active
|
||||
FROM users
|
||||
WHERE "id" = ${id}
|
||||
),
|
||||
application_data AS (
|
||||
SELECT
|
||||
id,
|
||||
posting_id AS "postingId",
|
||||
(SELECT title FROM postings WHERE id = posting_id) AS "postingTitle",
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt"
|
||||
FROM applications
|
||||
WHERE "user_id" = ${id}
|
||||
)
|
||||
SELECT
|
||||
(
|
||||
SELECT row_to_json(company_data)
|
||||
FROM company_data
|
||||
) AS company,
|
||||
(
|
||||
SELECT row_to_json(user_data)
|
||||
FROM user_data
|
||||
) AS user,
|
||||
(
|
||||
SELECT json_agg(row_to_json(application_data))
|
||||
FROM application_data
|
||||
) AS applications;
|
||||
`;
|
||||
|
||||
if (!data) {
|
||||
error(404, 'User not found');
|
||||
}
|
||||
let user = data[0].user;
|
||||
user.company = data[0].company;
|
||||
|
||||
user.createdAt = new Date(user.createdAt);
|
||||
user.lastSignIn = new Date(user.lastSignIn);
|
||||
if (user.company) {
|
||||
user.company.createdAt = new Date(user.company.createdAt);
|
||||
}
|
||||
let applications = data[0].applications;
|
||||
if (applications) {
|
||||
applications.forEach((application: { createdAt: string | number | Date }) => {
|
||||
application.createdAt = new Date(application.createdAt);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
user: <User>user,
|
||||
applications: <Application[]>applications
|
||||
};
|
||||
}
|
||||
|
||||
// should require MANAGE_USERS permission
|
||||
export async function deleteUser(id: number): Promise<void> {
|
||||
const response = await sql`
|
||||
await sql`
|
||||
DELETE FROM users
|
||||
WHERE id = ${id};
|
||||
`;
|
||||
@ -177,16 +274,456 @@ export async function updateLastSignin(username: string): Promise<void> {
|
||||
`;
|
||||
}
|
||||
|
||||
export async function createCompany(
|
||||
name: string,
|
||||
description: string,
|
||||
website: string
|
||||
): Promise<number> {
|
||||
export async function createCompany(company: Company): Promise<number> {
|
||||
const response = await sql`
|
||||
INSERT INTO companies (name, description, website, created_at, company_code)
|
||||
VALUES (${name}, ${description}, ${website}, NOW(), generate_company_code(CAST(CURRVAL('companies_id_seq') AS INT)))
|
||||
VALUES (${company.name}, ${company.description}, ${company.website}, NOW(), generate_company_code(CAST(CURRVAL('companies_id_seq') AS INT)))
|
||||
RETURNING id;
|
||||
`;
|
||||
|
||||
return response[0].id;
|
||||
}
|
||||
|
||||
export async function editCompany(company: Company): Promise<number> {
|
||||
const response = await sql`
|
||||
UPDATE companies
|
||||
SET name = ${company.name}, description = ${company.description}, website = ${company.website}
|
||||
WHERE id = ${company.id}
|
||||
RETURNING id;
|
||||
`;
|
||||
|
||||
return response[0].id;
|
||||
}
|
||||
|
||||
export async function deleteCompany(id: number): Promise<void> {
|
||||
await sql`
|
||||
DELETE FROM companies
|
||||
WHERE id = ${id};
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getCompany(id: number): Promise<Company> {
|
||||
const [company] = await sql`
|
||||
SELECT id, name, description, website, created_at AS "createdAt"
|
||||
FROM companies
|
||||
WHERE id = ${id};
|
||||
`;
|
||||
|
||||
if (!company) {
|
||||
error(404, 'Company not found');
|
||||
}
|
||||
|
||||
return <Company>company;
|
||||
}
|
||||
|
||||
export async function getCompanyFullData(
|
||||
id: number
|
||||
): Promise<{ company: Company; users: User[]; postings: Posting[] }> {
|
||||
const data = await sql`
|
||||
WITH company_data AS (
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
website,
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt"
|
||||
FROM companies
|
||||
WHERE id = ${id}
|
||||
),
|
||||
user_data AS (
|
||||
SELECT
|
||||
username,
|
||||
email,
|
||||
phone,
|
||||
full_name AS "fullName"
|
||||
FROM users
|
||||
WHERE "company_id" = ${id}
|
||||
),
|
||||
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 "company_id" = ${id}
|
||||
)
|
||||
SELECT
|
||||
(
|
||||
SELECT row_to_json(company_data)
|
||||
FROM company_data
|
||||
) AS company,
|
||||
(
|
||||
SELECT json_agg(row_to_json(user_data))
|
||||
FROM user_data
|
||||
) AS users,
|
||||
(
|
||||
SELECT json_agg(row_to_json(posting_data))
|
||||
FROM posting_data
|
||||
) AS postings;
|
||||
`;
|
||||
|
||||
if (!data) {
|
||||
error(404, 'Company not found');
|
||||
}
|
||||
if (data[0].company) {
|
||||
data[0].company.createdAt = new Date(data[0].company.createdAt);
|
||||
}
|
||||
if (data[0].postings) {
|
||||
data[0].postings.forEach(
|
||||
(posting: {
|
||||
createdAt: string | number | Date;
|
||||
updatedAt: string | number | Date;
|
||||
tagIds: number[] | undefined;
|
||||
tags: { id: number; displayName: null; createdAt: null }[];
|
||||
}) => {
|
||||
posting.createdAt = new Date(posting.createdAt);
|
||||
posting.updatedAt = new Date(posting.updatedAt);
|
||||
if (posting.tagIds) {
|
||||
posting.tagIds?.forEach((tagId: number) => {
|
||||
posting.tags.push({ id: tagId, displayName: null, createdAt: null });
|
||||
});
|
||||
}
|
||||
delete posting.tagIds;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
company: <Company>data[0].company,
|
||||
users: <User[]>data[0].users,
|
||||
postings: <Posting[]>data[0].postings
|
||||
};
|
||||
}
|
||||
|
||||
export async function createPosting(posting: Posting): Promise<number> {
|
||||
if (posting.tagIds === null || posting.tagIds === undefined) {
|
||||
posting.tagIds = [];
|
||||
}
|
||||
posting.tags?.forEach((tag) => {
|
||||
posting.tagIds?.push(tag.id);
|
||||
});
|
||||
if (posting.companyId === null || posting.companyId === undefined) {
|
||||
if (posting.company) {
|
||||
posting.companyId = posting.company.id;
|
||||
} else {
|
||||
posting.companyId = null;
|
||||
}
|
||||
}
|
||||
const response = await sql`
|
||||
INSERT INTO postings (title, description, employer_id, address, employment_type, wage, link, tag_ids, created_at, updated_at, flyer_link, company_id)
|
||||
VALUES (${posting.title}, ${posting.description}, ${posting.employerId}, ${posting.address}, ${posting.employmentType}, ${posting.wage}, ${posting.link}, ${posting.tagIds}, NOW(), NOW(), ${posting.flyerLink}, ${posting.companyId})
|
||||
RETURNING id;
|
||||
`;
|
||||
|
||||
return response[0].id;
|
||||
}
|
||||
|
||||
export async function editPosting(posting: Posting): Promise<number> {
|
||||
if (posting.tagIds === null || posting.tagIds === undefined) {
|
||||
posting.tagIds = [];
|
||||
}
|
||||
posting.tags?.forEach((tag) => {
|
||||
posting.tagIds?.push(tag.id);
|
||||
});
|
||||
if (posting.companyId === null || posting.companyId === undefined) {
|
||||
if (posting.company) {
|
||||
posting.companyId = posting.company.id;
|
||||
} else {
|
||||
posting.companyId = null;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await sql`
|
||||
UPDATE postings
|
||||
SET title = ${posting.title}, description = ${posting.description}, employer_id = ${posting.employerId}, address = ${posting.address}, employment_type = ${posting.employmentType}, wage = ${posting.wage}, link = ${posting.link}, tag_ids = ${posting.tagIds}, updated_at = NOW(), flyer_link = ${posting.flyerLink}, company_id = ${posting.companyId}
|
||||
WHERE id = ${posting.id}
|
||||
RETURNING id;
|
||||
`;
|
||||
|
||||
return response[0].id;
|
||||
}
|
||||
|
||||
export async function deletePosting(id: number): Promise<void> {
|
||||
await sql`
|
||||
DELETE FROM postings
|
||||
WHERE id = ${id};
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getCompanyEmployers(
|
||||
id: number
|
||||
): Promise<{ company: Company; users: User[] }> {
|
||||
const data = await sql`
|
||||
WITH company_data AS (
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
website,
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
company_code AS "companyCode"
|
||||
FROM companies
|
||||
WHERE id = ${id}
|
||||
),
|
||||
user_data AS (SELECT id,
|
||||
username,
|
||||
email,
|
||||
phone,
|
||||
full_name AS "fullName",
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
last_signin AT TIME ZONE 'UTC' AS "lastSignIn",
|
||||
company_id as "companyId"
|
||||
FROM users
|
||||
WHERE "company_code" = (SELECT company_code FROM companies WHERE id = ${id}))
|
||||
SELECT
|
||||
(
|
||||
SELECT row_to_json(company_data)
|
||||
FROM company_data
|
||||
) AS company,
|
||||
(
|
||||
SELECT json_agg(row_to_json(user_data))
|
||||
FROM user_data
|
||||
) AS users;
|
||||
`;
|
||||
|
||||
if (!data) {
|
||||
error(404, 'Company not found');
|
||||
}
|
||||
if (data[0].users) {
|
||||
data[0].users.forEach(
|
||||
(user: {
|
||||
company: { id: any };
|
||||
companyId: any;
|
||||
createdAt: string | number | Date;
|
||||
lastSignIn: string | number | Date;
|
||||
}) => {
|
||||
user.company = {
|
||||
id: user.companyId
|
||||
};
|
||||
user.createdAt = new Date(user.createdAt);
|
||||
user.lastSignIn = new Date(user.lastSignIn);
|
||||
delete user.companyId;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
company: <Company>data[0].company,
|
||||
users: <User[]>data[0].users
|
||||
};
|
||||
}
|
||||
|
||||
export async function removeEmployerFromCompany(companyId: number, userId: number): Promise<void> {
|
||||
await sql`
|
||||
UPDATE users
|
||||
SET company_id = NULL,
|
||||
company_code = NULL
|
||||
WHERE id = ${userId};
|
||||
`;
|
||||
}
|
||||
|
||||
export async function addEmployerToCompany(companyId: number, userId: number): Promise<void> {
|
||||
await sql`
|
||||
UPDATE users
|
||||
SET company_id = ${companyId}
|
||||
WHERE id = ${userId};
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getPostings(searchQuery: string | null = null): Promise<Posting[]> {
|
||||
const postings = await sql<Posting[]>`
|
||||
SELECT p.id,
|
||||
p.title,
|
||||
p.description,
|
||||
p.employer_id AS "employerId",
|
||||
p.address,
|
||||
p.employment_type AS "employmentType",
|
||||
p.wage,
|
||||
p.link,
|
||||
p.tag_ids AS "tagIds",
|
||||
p.created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
p.updated_at AT TIME ZONE 'UTC' AS "updatedAt",
|
||||
p.flyer_link AS "flyerLink",
|
||||
p.company_id AS "companyId",
|
||||
c.name AS "companyName"
|
||||
FROM postings p
|
||||
LEFT JOIN companies c ON p.company_id = c.id
|
||||
WHERE title ILIKE ${searchQuery ? `%${searchQuery}%` : '%'};
|
||||
`;
|
||||
postings.forEach((posting) => {
|
||||
posting.company = <Company>{};
|
||||
if (posting.companyName) {
|
||||
posting.company.name = posting.companyName;
|
||||
}
|
||||
delete posting.companyName;
|
||||
posting.tags = [];
|
||||
|
||||
posting.employmentType = EmploymentType[posting.employmentType as keyof typeof EmploymentType];
|
||||
if (posting.tagIds) {
|
||||
posting.tagIds?.forEach((tagId: number) => {
|
||||
posting.tags.push({ id: tagId, displayName: null, createdAt: null });
|
||||
});
|
||||
}
|
||||
delete posting.tagIds;
|
||||
});
|
||||
return <Posting[]>(<unknown>postings);
|
||||
}
|
||||
|
||||
export async function getPosting(id: number): Promise<Posting> {
|
||||
const data = await sql<Posting[]>`
|
||||
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",
|
||||
company_id AS "companyId"
|
||||
FROM postings
|
||||
WHERE id = ${id};
|
||||
`;
|
||||
const posting = data[0];
|
||||
posting.tags = [];
|
||||
posting.employmentType = EmploymentType[posting.employmentType as keyof typeof EmploymentType];
|
||||
if (posting.tagIds) {
|
||||
posting.tagIds?.forEach((tagId: number) => {
|
||||
posting.tags.push({ id: tagId, displayName: null, createdAt: null });
|
||||
});
|
||||
}
|
||||
delete posting.tagIds;
|
||||
|
||||
if (posting.createdAt) {
|
||||
posting.createdAt = new Date(posting.createdAt);
|
||||
}
|
||||
if (posting.updatedAt) {
|
||||
posting.updatedAt = new Date(posting.updatedAt);
|
||||
}
|
||||
|
||||
return posting;
|
||||
}
|
||||
|
||||
export async function getPostingFullData(id: number): Promise<Posting> {
|
||||
const data = await sql`
|
||||
WITH company_data AS (
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
website,
|
||||
created_at AS "createdAt"
|
||||
FROM companies
|
||||
WHERE id = (SELECT company_id FROM postings WHERE id = ${id})
|
||||
),
|
||||
user_data AS (
|
||||
SELECT
|
||||
username,
|
||||
email,
|
||||
phone,
|
||||
full_name AS "fullName"
|
||||
FROM users
|
||||
WHERE "company_id" = (SELECT company_id FROM postings WHERE id = ${id})
|
||||
),
|
||||
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 = ${id}
|
||||
)
|
||||
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;
|
||||
`;
|
||||
|
||||
if (!data) {
|
||||
error(404, 'Posting not found');
|
||||
}
|
||||
let 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);
|
||||
}
|
||||
|
||||
return posting;
|
||||
}
|
||||
|
||||
export async function createApplication(application: Application): Promise<number> {
|
||||
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;
|
||||
`;
|
||||
|
||||
return response[0].id;
|
||||
}
|
||||
|
||||
export async function deleteApplication(id: number): Promise<void> {
|
||||
const response = await sql`
|
||||
DELETE FROM applications
|
||||
WHERE id = ${id};
|
||||
`;
|
||||
}
|
||||
|
||||
export async function deleteApplicationWithUser(
|
||||
applicationId: number,
|
||||
userId: number
|
||||
): Promise<void> {
|
||||
console.log(applicationId, userId);
|
||||
const response = await sql`
|
||||
DELETE FROM applications
|
||||
WHERE id = ${applicationId} AND user_id = ${userId};
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getApplications(postingId: number): Promise<Application[]> {
|
||||
const applications = await sql<Application[]>`
|
||||
SELECT id, posting_id AS "postingId", user_id AS "userId", candidate_statement AS "candidateStatement", created_at AS "createdAt"
|
||||
FROM applications
|
||||
WHERE posting_id = ${postingId};
|
||||
`;
|
||||
|
||||
applications.forEach((application) => {
|
||||
application.createdAt = new Date(application.createdAt);
|
||||
});
|
||||
|
||||
return applications;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
import { type Cookies, error } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { getUserWithCompany } from '$lib/db/index.server';
|
||||
import type { User } from '$lib/types';
|
||||
|
||||
// TODO: Handle saving custom avatar uploads
|
||||
export async function saveAvatar(user: User): Promise<void> {
|
||||
@ -14,6 +14,7 @@ export async function saveAvatar(user: User): Promise<void> {
|
||||
fs.writeFileSync(filePath, avatar);
|
||||
}
|
||||
|
||||
// TODO: change to return null instead of -1
|
||||
export function getUserPerms(cookies: Cookies): number {
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw new Error('JWT_SECRET not defined');
|
||||
@ -51,3 +52,22 @@ export function getUserId(cookies: Cookies): number {
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
export function getUserCompanyId(cookies: Cookies): number | null {
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw new Error('JWT_SECRET not defined');
|
||||
}
|
||||
|
||||
const JWT = cookies.get('jwt');
|
||||
if (JWT) {
|
||||
try {
|
||||
const decoded = jwt.verify(JWT, process.env.JWT_SECRET);
|
||||
if (typeof decoded === 'object' && 'companyId' in decoded) {
|
||||
return decoded['companyId'];
|
||||
}
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
}
|
||||
|
||||
20
src/lib/shared.server.ts
Normal file
20
src/lib/shared.server.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type { User } from '$lib/types';
|
||||
|
||||
export function setJWT(cookies: Cookies, user: User) {
|
||||
const payload = {
|
||||
username: user.username,
|
||||
perms: user.perms,
|
||||
id: user.id,
|
||||
companyId: user.companyId
|
||||
};
|
||||
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw new Error('JWT_SECRET not defined');
|
||||
}
|
||||
|
||||
const maxAge = 60 * 60 * 24 * 30; // 30 days
|
||||
const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' });
|
||||
cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false });
|
||||
}
|
||||
@ -1,6 +1,12 @@
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import type { EmploymentType } from '$lib/types';
|
||||
|
||||
export let userState = $state({ perms: PERMISSIONS.VIEW, username: null, id: null });
|
||||
export let userState = $state({
|
||||
perms: PERMISSIONS.VIEW,
|
||||
username: null,
|
||||
id: null,
|
||||
companyId: null
|
||||
});
|
||||
|
||||
export const userPerms = PERMISSIONS.VIEW | PERMISSIONS.APPLY_FOR_JOBS;
|
||||
export const employerPerms = PERMISSIONS.SUBMIT_POSTINGS | PERMISSIONS.MANAGE_EMPLOYERS;
|
||||
@ -19,3 +25,28 @@ export function telFormatter(initial: string) {
|
||||
(num.length > 6 ? '-' + num.substring(6, 10) : '');
|
||||
return initial;
|
||||
}
|
||||
|
||||
export const getCookieValue = (name: String) =>
|
||||
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
|
||||
|
||||
export function updateUserState() {
|
||||
const JWT = getCookieValue('jwt');
|
||||
if (JWT !== '') {
|
||||
const state = JSON.parse(atob(JWT.split('.')[1]));
|
||||
userState.perms = state.perms;
|
||||
userState.username = state.username;
|
||||
userState.id = state.id;
|
||||
userState.companyId = state.companyId;
|
||||
}
|
||||
}
|
||||
|
||||
export function employmentTypeDisplayName(type: EmploymentType) {
|
||||
switch (type) {
|
||||
case 'full_time':
|
||||
return 'Full Time';
|
||||
case 'part_time':
|
||||
return 'Part Time';
|
||||
case 'internship':
|
||||
return 'Internship';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
interface User {
|
||||
export interface User {
|
||||
id: number | null;
|
||||
username: string;
|
||||
password: string | null;
|
||||
@ -11,18 +11,58 @@ interface User {
|
||||
fullName: string | null;
|
||||
company: Company | null;
|
||||
companyCode: string | null;
|
||||
companyId: number | null | undefined;
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
export interface Company {
|
||||
id: number;
|
||||
displayName: string;
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
website: string | null;
|
||||
createdAt: Date | null;
|
||||
companyCode: string | null;
|
||||
}
|
||||
|
||||
interface Company {
|
||||
export interface Posting {
|
||||
id: number;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
website: string;
|
||||
employerId: number;
|
||||
address: string;
|
||||
employmentType: EmploymentType;
|
||||
wage: string;
|
||||
link: string;
|
||||
tags: Tag[];
|
||||
tagIds: number[] | null | undefined;
|
||||
createdAt: Date | null;
|
||||
updatedAt: Date | null;
|
||||
flyerLink: string | null;
|
||||
company: Company;
|
||||
companyId: number | null | undefined;
|
||||
companyName: string | null | undefined;
|
||||
employer: User | null | undefined;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: number;
|
||||
displayName: string | null;
|
||||
createdAt: Date | null;
|
||||
}
|
||||
|
||||
export enum EmploymentType {
|
||||
full_time = 'full_time',
|
||||
part_time = 'part_time',
|
||||
internship = 'internship'
|
||||
// contract = 'Contract',
|
||||
// temporary = 'Temporary',
|
||||
// seasonal = 'Seasonal'
|
||||
}
|
||||
|
||||
export interface Application {
|
||||
id: number;
|
||||
userId: number;
|
||||
postingId: number;
|
||||
postingTitle: string | null;
|
||||
candidateStatement: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { onMount } from 'svelte';
|
||||
import { userState } from '$lib/shared.svelte';
|
||||
import { updateUserState, userState } from '$lib/shared.svelte';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { updateUserState } from './utils.client';
|
||||
|
||||
let currentTheme: string = $state('');
|
||||
|
||||
@ -32,25 +31,15 @@
|
||||
updateUserState();
|
||||
});
|
||||
|
||||
if ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0) {
|
||||
('/admin/postings');
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_USERS) !== 0) {
|
||||
('/admin/employers');
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_TAGS) !== 0) {
|
||||
('/admin/tags');
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0) {
|
||||
('/admin/companies');
|
||||
}
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..40,400,0,0&display=block&icon_names=account_circle,arrow_drop_down,arrow_drop_up,calendar_today,call,check,close,dark_mode,edit,group,info,light_mode,login,mail,person,search,sell,store,visibility,visibility_off,work"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..40,400,0,0&display=block&icon_names=account_circle,arrow_drop_down,arrow_drop_up,calendar_today,call,check,close,dark_mode,delete,description,edit,group,info,light_mode,login,mail,person,search,sell,store,visibility,visibility_off,work"
|
||||
/>
|
||||
|
||||
<div class="bottom-border flex h-14 justify-between p-3 align-middle">
|
||||
<div class="bottom-border bg-color sticky top-0 flex h-14 justify-between p-3 align-middle">
|
||||
<nav class="pt-1">
|
||||
<a href="/" class="hover-bg-color mr-1 rounded-md px-2 pb-2 pt-1.5">
|
||||
<img
|
||||
@ -61,22 +50,23 @@
|
||||
width="24"
|
||||
/>
|
||||
</a>
|
||||
<a href="/about" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">About</a>
|
||||
{#if (userState.perms & PERMISSIONS.VIEW) !== 0}
|
||||
<a href="/listings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">Listings</a>
|
||||
<!-- <a href="/about" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">About</a>-->
|
||||
{#if (userState.perms & PERMISSIONS.VIEW) > 0}
|
||||
<a href="/postings" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">Postings</a>
|
||||
{/if}
|
||||
{#if (userState.perms & PERMISSIONS.VIEW) !== 0}
|
||||
{#if (userState.perms & PERMISSIONS.VIEW) > 0}
|
||||
<a href="/companies" class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">Companies</a>
|
||||
{/if}
|
||||
{#if (userState.perms & (PERMISSIONS.MANAGE_POSTINGS | PERMISSIONS.MANAGE_TAGS | PERMISSIONS.MANAGE_USERS)) !== 0}
|
||||
{#if (userState.perms & (PERMISSIONS.MANAGE_POSTINGS | PERMISSIONS.MANAGE_TAGS | PERMISSIONS.MANAGE_USERS)) > 0}
|
||||
<a
|
||||
href={(userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0
|
||||
href={(userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0
|
||||
? '/admin/postings'
|
||||
: (userState.perms & PERMISSIONS.MANAGE_USERS) !== 0
|
||||
: (userState.perms & PERMISSIONS.MANAGE_USERS) > 0
|
||||
? '/admin/users'
|
||||
: (userState.perms & PERMISSIONS.MANAGE_TAGS) !== 0
|
||||
? '/admin/tags'
|
||||
: (userState.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0
|
||||
: // TODO: Implement tags
|
||||
// : (userState.perms & PERMISSIONS.MANAGE_TAGS) > 0
|
||||
// ? '/admin/tags'
|
||||
(userState.perms & PERMISSIONS.MANAGE_COMPANIES) > 0
|
||||
? '/admin/companies'
|
||||
: '/admin'}
|
||||
class="hover-bg-color mr-1 rounded px-3 py-2 text-sm">Administration</a
|
||||
@ -89,11 +79,11 @@
|
||||
{currentTheme === 'light' ? 'light_mode' : 'dark_mode'}
|
||||
</span>
|
||||
</button>
|
||||
<button onclick={() => (window.location.href = userState.id !== null ? '/account' : '/signin')}>
|
||||
<a href={userState.id !== null ? '/account' : '/signin'}>
|
||||
<span class="material-symbols-outlined hover-bg-color rounded-full p-1">
|
||||
{userState.id !== null ? 'account_circle' : 'login'}
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { updateUserState } from './utils.client';
|
||||
|
||||
import { updateUserState } from '$lib/shared.svelte';
|
||||
|
||||
onMount(() => {
|
||||
updateUserState();
|
||||
|
||||
@ -1,10 +1,25 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getUserWithCompany } from '$lib/db/index.server';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { getUserId } from '$lib/index.server';
|
||||
import {
|
||||
deleteApplication,
|
||||
deleteApplicationWithUser,
|
||||
getUserWithCompanyAndApplications
|
||||
} from '$lib/db/index.server';
|
||||
import { getUserId, getUserPerms } from '$lib/index.server';
|
||||
import { type Actions, fail } from '@sveltejs/kit';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const id = getUserId(cookies);
|
||||
return { user: await getUserWithCompany(id) };
|
||||
return await getUserWithCompanyAndApplications(id);
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
delete: async ({ url, cookies }) => {
|
||||
const id = parseInt(url.searchParams.get('id')!);
|
||||
try {
|
||||
await deleteApplicationWithUser(id, getUserId(cookies));
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let applicationToDelete: number = $state(0);
|
||||
|
||||
onMount(() => {
|
||||
if (!document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/signin';
|
||||
@ -29,6 +31,14 @@
|
||||
window.location.href = '/signin';
|
||||
}
|
||||
|
||||
function openConfirm() {
|
||||
document.getElementById('deleteConfirmModal')!.style.display = 'block';
|
||||
}
|
||||
|
||||
function closeConfirm() {
|
||||
document.getElementById('deleteConfirmModal')!.style.display = 'none';
|
||||
}
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
@ -39,7 +49,9 @@
|
||||
<img
|
||||
id="avatar"
|
||||
class="mb-2 inline-block rounded-lg"
|
||||
src="/uploads/avatars/{data.user.id}.svg?timestamp=${Date.now()}"
|
||||
src="/uploads/avatars/{data.user.id
|
||||
? data.user.id
|
||||
: 'default'}.svg?timestamp=${Date.now()}"
|
||||
onerror={avatarFallback}
|
||||
alt="User avatar"
|
||||
height="240"
|
||||
@ -72,7 +84,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="elevated separator-borders m-2 inline-block h-min w-full rounded">
|
||||
<div class="inline-block w-full">
|
||||
<div class="elevated separator-borders m-2 h-min w-full rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">User Details</div>
|
||||
<div class="flex">
|
||||
@ -128,5 +141,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if data.applications}
|
||||
<div class="elevated separator-borders m-2 inline-block h-min w-full rounded">
|
||||
<div class="p-3 font-semibold">Pending applications</div>
|
||||
{#each data.applications as application}
|
||||
<div class="top-border flex justify-between p-3">
|
||||
<div class="inline-block">
|
||||
<div class="font-semibold">
|
||||
Applied to: <span class="font-normal">{application.postingTitle}</span>
|
||||
</div>
|
||||
<div class="font-semibold">
|
||||
Applied on: <span class="font-normal"
|
||||
>{data.applications[0].createdAt.toLocaleDateString(
|
||||
'en-US',
|
||||
dateFormatOptions
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="material-symbols-outlined danger-color inline-block"
|
||||
onclick={() => {
|
||||
applicationToDelete = application.id;
|
||||
openConfirm();
|
||||
}}>delete</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form id="deleteConfirmModal" method="POST" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="mb-2 inline-flex w-full justify-between">
|
||||
<h2 class="font-semibold">Are you sure?</h2>
|
||||
<button class="material-symbols-outlined" onclick={closeConfirm}>close</button>
|
||||
</div>
|
||||
<p>This will permanently delete this application. This action cannot be undone.</p>
|
||||
|
||||
<div class="mt-4 flex justify-between">
|
||||
<button
|
||||
class="danger-bg-color rounded px-2 py-1"
|
||||
type="submit"
|
||||
formaction="?/delete&id={applicationToDelete}">Delete application</button
|
||||
>
|
||||
<button
|
||||
class="separator-borders bg-color rounded px-2 py-1"
|
||||
type="button"
|
||||
onclick={closeConfirm}>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -3,6 +3,7 @@ import { deleteUser, getUser, getUserWithCompany, updateUser } from '$lib/db/ind
|
||||
import { type Actions, fail, redirect } from '@sveltejs/kit';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { getUserId } from '$lib/index.server';
|
||||
import type { User } from '$lib/types';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const id = getUserId(cookies);
|
||||
@ -23,9 +24,7 @@ export const actions: Actions = {
|
||||
.toUpperCase()
|
||||
.trim();
|
||||
|
||||
if (email === '' || email == undefined) email = null;
|
||||
if (phone === '' || phone == undefined) phone = null;
|
||||
if (fullName === '' || fullName == undefined) fullName = null;
|
||||
if (companyCode === '' || companyCode == undefined) companyCode = null;
|
||||
|
||||
if (email && !email.includes('@')) {
|
||||
@ -36,7 +35,7 @@ export const actions: Actions = {
|
||||
return fail(400, { errorMessage: 'Invalid phone number' });
|
||||
}
|
||||
|
||||
if (username && username !== '') {
|
||||
if (username && username !== '' && fullName && fullName !== '' && email && email !== '') {
|
||||
try {
|
||||
await updateUser(<User>{
|
||||
id: id,
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
let permsAccordions: boolean[] = [false, false, false];
|
||||
|
||||
onMount(() => {
|
||||
if (!document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/signin';
|
||||
}
|
||||
|
||||
let acc = document.getElementsByClassName('accordion');
|
||||
for (let i = 0; i < acc.length; i++) {
|
||||
acc[i].addEventListener('click', function (this: HTMLElement, event: Event) {
|
||||
@ -89,7 +93,7 @@
|
||||
</div>
|
||||
<form method="POST" class="px-4" autocomplete="off" use:enhance>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Username
|
||||
Username <span class="danger-color">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
@ -100,7 +104,19 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email (optional)
|
||||
Full name <span class="danger-color">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
value={data.user?.fullName}
|
||||
placeholder="Full name"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email <span class="danger-color">*</span>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
@ -108,6 +124,7 @@
|
||||
value={data.user?.email}
|
||||
placeholder="Email"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
@ -122,17 +139,6 @@
|
||||
pattern="([0-9]\{3}) [0-9]\{3}-[0-9]\{3}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Full name (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
value={data.user?.fullName}
|
||||
placeholder="Full name"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Company code (optional)
|
||||
<input
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import '../../app.css';
|
||||
import { page } from '$app/state';
|
||||
import { userState } from '$lib/shared.svelte';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
@ -8,7 +7,7 @@
|
||||
</script>
|
||||
|
||||
<div class="bottom-border h-10 pt-2 text-center">
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0}
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0}
|
||||
<a
|
||||
href="/admin/postings"
|
||||
class="p-2 {page.url.pathname.startsWith('/admin/postings')
|
||||
@ -17,7 +16,7 @@
|
||||
><span class="material-symbols-outlined align-bottom">work</span> Postings</a
|
||||
>
|
||||
{/if}
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_USERS) !== 0}
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_USERS) > 0}
|
||||
<a
|
||||
href="/admin/users"
|
||||
class="p-2 {page.url.pathname.startsWith('/admin/users')
|
||||
@ -26,16 +25,17 @@
|
||||
><span class="material-symbols-outlined align-bottom">group</span> Users</a
|
||||
>
|
||||
{/if}
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_TAGS) !== 0}
|
||||
<a
|
||||
href="/admin/tags"
|
||||
class="{page.url.pathname.startsWith('/admin/tags')
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text low-emphasis-text-button'} p-2"
|
||||
><span class="material-symbols-outlined align-bottom">sell</span> Tags</a
|
||||
>
|
||||
{/if}
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0}
|
||||
<!--TODO-->
|
||||
<!--{#if (userState.perms & PERMISSIONS.MANAGE_TAGS) > 0}-->
|
||||
<!-- <a-->
|
||||
<!-- href="/admin/tags"-->
|
||||
<!-- class="{page.url.pathname.startsWith('/admin/tags')-->
|
||||
<!-- ? 'primary-underline font-bold'-->
|
||||
<!-- : 'low-emphasis-text low-emphasis-text-button'} p-2"-->
|
||||
<!-- ><span class="material-symbols-outlined align-bottom">sell</span> Tags</a-->
|
||||
<!-- >-->
|
||||
<!--{/if}-->
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_COMPANIES) > 0}
|
||||
<a
|
||||
href="/admin/companies"
|
||||
class="{page.url.pathname.startsWith('/admin/companies')
|
||||
|
||||
@ -4,13 +4,13 @@
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
|
||||
onMount(() => {
|
||||
if ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0) {
|
||||
if ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0) {
|
||||
window.location.href = '/admin/postings';
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_USERS) !== 0) {
|
||||
window.location.href = '/admin/employers';
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_TAGS) !== 0) {
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_USERS) > 0) {
|
||||
window.location.href = '/admin/users';
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_TAGS) > 0) {
|
||||
window.location.href = '/admin/tags';
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0) {
|
||||
} else if ((userState.perms & PERMISSIONS.MANAGE_COMPANIES) > 0) {
|
||||
window.location.href = '/admin/companies';
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getCompanies, getUsers } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getUserPerms } from '$lib/index.server';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
const search = url.searchParams.get('searchCompanies');
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_COMPANIES) > 0) {
|
||||
return {
|
||||
companies: await getCompanies(search)
|
||||
};
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
};
|
||||
@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
|
||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">
|
||||
Company Management (Total: {data.companies?.length || 0})
|
||||
</div>
|
||||
<a class="dull-primary-bg-color m-2 rounded-md px-2.5 py-1" href="/companies/create"
|
||||
>Create new company</a
|
||||
>
|
||||
</div>
|
||||
<form action="" class="px-4">
|
||||
<div class="flex py-4">
|
||||
<div class="search-bar">
|
||||
<input
|
||||
type="search"
|
||||
name="searchCompanies"
|
||||
id="searchCompanies"
|
||||
placeholder="Search Companies"
|
||||
class="search-cancel"
|
||||
/>
|
||||
<button><span class="material-symbols-outlined">search</span></button>
|
||||
</div>
|
||||
<button class="hover-bg-color mx-2 rounded py-2 pl-3 pr-2 text-sm"
|
||||
>Filter<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||
></button
|
||||
>
|
||||
<button class="hover-bg-color rounded py-2 pl-3 pr-2 text-sm"
|
||||
>Sort<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left w-16 py-1">ID</th>
|
||||
<th class="py-1">Name</th>
|
||||
<th class="py-1">Website</th>
|
||||
<th class="py-1">Created</th>
|
||||
<th class="w-28 py-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if data.companies !== undefined}
|
||||
{#each data.companies as company}
|
||||
<tr>
|
||||
<td class="left">{company.id}</td>
|
||||
<td>{company.name}</td>
|
||||
<td>{company.website}</td>
|
||||
<td
|
||||
>{company.createdAt?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td class="w-28 pr-1 text-end">
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined hyperlink-color icon-20 tooltip relative my-1 rounded p-1"
|
||||
href="/companies/{company.id}"
|
||||
>store<span class="tooltip-text font-sans text-sm">View company</span></a
|
||||
>
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined icon-20 hyperlink-color tooltip relative my-1 ml-1 mr-8 rounded p-1"
|
||||
href="/companies/{company.id}/edit"
|
||||
>edit<span class="tooltip-text font-sans text-sm">Edit company</span></a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
16
src/routes/admin/postings/+page.server.ts
Normal file
16
src/routes/admin/postings/+page.server.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getPostings, getUsers } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getUserPerms } from '$lib/index.server';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
const search = url.searchParams.get('searchUsers');
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_POSTINGS) > 0) {
|
||||
return {
|
||||
postings: await getPostings(search)
|
||||
};
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
};
|
||||
@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { employmentTypeDisplayName } from '$lib/shared.svelte';
|
||||
|
||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">
|
||||
Posting Management (Total: {data.postings?.length || 0})
|
||||
</div>
|
||||
<a class="dull-primary-bg-color m-2 rounded-md px-2.5 py-1" href="/postings/create"
|
||||
>Create new posting</a
|
||||
>
|
||||
</div>
|
||||
<form action="" class="px-4">
|
||||
<div class="flex py-4">
|
||||
<div class="search-bar">
|
||||
<input
|
||||
type="search"
|
||||
name="searchUsers"
|
||||
id="searchUsers"
|
||||
placeholder="Search Users"
|
||||
class="search-cancel"
|
||||
/>
|
||||
<button><span class="material-symbols-outlined">search</span></button>
|
||||
</div>
|
||||
<button class="hover-bg-color mx-2 rounded py-2 pl-3 pr-2 text-sm"
|
||||
>Filter<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||
></button
|
||||
>
|
||||
<button class="hover-bg-color rounded py-2 pl-3 pr-2 text-sm"
|
||||
>Sort<span class="material-symbols-outlined icon-20 align-middle">arrow_drop_down</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left w-16 py-1">ID</th>
|
||||
<th class="py-1">Title</th>
|
||||
<th class="py-1">Company</th>
|
||||
<th class="py-1">Created</th>
|
||||
<th class="py-1">Updated</th>
|
||||
<th class="py-1">Employment Type</th>
|
||||
<th class="w-28 py-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if data.postings !== undefined}
|
||||
{#each data.postings as posting}
|
||||
<tr>
|
||||
<td class="left">{posting.id}</td>
|
||||
<td class="left">{posting.title}</td>
|
||||
<td>{posting.company.name || 'unknown'}</td>
|
||||
<td
|
||||
>{posting.createdAt?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td
|
||||
>{posting.updatedAt?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td>{employmentTypeDisplayName(posting.employmentType)}</td>
|
||||
<td class="w-28 pr-1 text-end">
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined hyperlink-color icon-20 tooltip relative my-1 rounded p-1"
|
||||
href="/postings/{posting.id}"
|
||||
>work<span class="tooltip-text font-sans text-sm">View posting</span></a
|
||||
>
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined icon-20 hyperlink-color tooltip relative my-1 ml-1 mr-8 rounded p-1"
|
||||
href="/postings/{posting.id}/manage/edit"
|
||||
>edit<span class="tooltip-text font-sans text-sm">Edit posting</span></a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -59,11 +59,11 @@
|
||||
<td>{tag.createdAt?.toLocaleDateString('en-US', dateFormatOptions)}</td>
|
||||
<td class="w-28 pr-1 text-end">
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined icon-20 my-1 rounded p-1"
|
||||
class="hover-bg-color material-symbols-outlined hyperlink-color icon-20 my-1 rounded p-1"
|
||||
href="/admin/tags/{tag.id}">info</a
|
||||
>
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined icon-20 my-1 ml-1 mr-8 rounded p-1"
|
||||
class="hover-bg-color material-symbols-outlined hyperlink-color icon-20 my-1 ml-1 mr-8 rounded p-1"
|
||||
href="/admin/tags/{tag.id}/edit">edit</a
|
||||
>
|
||||
</td>
|
||||
|
||||
@ -75,17 +75,17 @@
|
||||
>{user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td class="material-symbols-outlined py-2">
|
||||
<td class="material-symbols-outlined py-1.5">
|
||||
{user.active ? 'check' : 'close'}
|
||||
</td>
|
||||
<td class="w-28 pr-1 text-end">
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined dull-primary-text-color icon-20 tooltip my-1 rounded p-1"
|
||||
class="hover-bg-color material-symbols-outlined hyperlink-color icon-20 tooltip relative my-1 rounded p-1"
|
||||
href="/admin/users/{user.id}"
|
||||
>person<span class="tooltip-text font-sans text-sm">View account</span></a
|
||||
>
|
||||
<a
|
||||
class="hover-bg-color material-symbols-outlined icon-20 dull-primary-text-color tooltip my-1 ml-1 mr-8 rounded p-1"
|
||||
class="hover-bg-color material-symbols-outlined icon-20 hyperlink-color tooltip relative my-1 ml-1 mr-8 rounded p-1"
|
||||
href="/admin/users/{user.id}/edit"
|
||||
>edit<span class="tooltip-text font-sans text-sm">Edit account</span></a
|
||||
>
|
||||
|
||||
@ -110,36 +110,36 @@
|
||||
<div class="top-border pt-2 font-semibold">
|
||||
Permissions: <span class="font-normal">{data.user.perms}</span>
|
||||
<div class="font-normal">
|
||||
{#if (data.user.perms & userPerms) !== 0}
|
||||
{#if (data.user.perms & userPerms) > 0}
|
||||
<p class="font-semibold">User permissions:</p>
|
||||
{#if (data.user.perms & PERMISSIONS.VIEW) !== 0}
|
||||
{#if (data.user.perms & PERMISSIONS.VIEW) > 0}
|
||||
<p class="pl-4">View access</p>
|
||||
{/if}
|
||||
{#if (data.user.perms & PERMISSIONS.APPLY_FOR_JOBS) !== 0}
|
||||
{#if (data.user.perms & PERMISSIONS.APPLY_FOR_JOBS) > 0}
|
||||
<p class="pl-4">Apply for jobs</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (data.user.perms & employerPerms) !== 0}
|
||||
{#if (data.user.perms & employerPerms) > 0}
|
||||
<p class="font-semibold">Employer permissions:</p>
|
||||
{#if (data.user.perms & PERMISSIONS.SUBMIT_POSTINGS) !== 0}
|
||||
{#if (data.user.perms & PERMISSIONS.SUBMIT_POSTINGS) > 0}
|
||||
<p class="pl-4">Submit postings</p>
|
||||
{/if}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_EMPLOYERS) !== 0}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_EMPLOYERS) > 0}
|
||||
<p class="pl-4">Manage employers (of their company)</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (data.user.perms & adminPerms) !== 0}
|
||||
{#if (data.user.perms & adminPerms) > 0}
|
||||
<p class="font-semibold">Admin permissions:</p>
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_POSTINGS) !== 0}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_POSTINGS) > 0}
|
||||
<p class="pl-4">Manage postings</p>
|
||||
{/if}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_USERS) !== 0}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_USERS) > 0}
|
||||
<p class="pl-4">Manage users</p>
|
||||
{/if}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_TAGS) !== 0}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_TAGS) > 0}
|
||||
<p class="pl-4">Manage tags</p>
|
||||
{/if}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_COMPANIES) !== 0}
|
||||
{#if (data.user.perms & PERMISSIONS.MANAGE_COMPANIES) > 0}
|
||||
<p class="pl-4">Manage companies</p>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@ -3,7 +3,8 @@ import { deleteUser, getUser, updateUser } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getUserPerms } from '$lib/index.server';
|
||||
import { userPerms } from '$lib/shared.svelte';
|
||||
import { userPerms, employerPerms } from '$lib/shared.svelte';
|
||||
import type { User } from '$lib/types';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, params }) => {
|
||||
const id = parseInt(params.user);
|
||||
@ -66,13 +67,22 @@ export const actions: Actions = {
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (!(requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_USERS) > 0)) {
|
||||
return fail(403, { errorMessage: 'Unauthorized' });
|
||||
} else {
|
||||
if (((requestPerms | userPerms) & newUserPerms) !== newUserPerms) {
|
||||
}
|
||||
if (((requestPerms | userPerms | employerPerms) & newUserPerms) !== newUserPerms) {
|
||||
return fail(403, {
|
||||
errorMessage: 'Cannot give a user higher permissions than yourself!'
|
||||
});
|
||||
} else {
|
||||
if (username && username !== '') {
|
||||
}
|
||||
if (!username) {
|
||||
return fail(400, { errorMessage: 'Missing username' });
|
||||
}
|
||||
if (password && password.length < 8) {
|
||||
return fail(400, { errorMessage: 'Password must be at least 8 characters' });
|
||||
}
|
||||
if (username.length < 4) {
|
||||
return fail(400, { errorMessage: 'Username must be at least 4 characters' });
|
||||
}
|
||||
|
||||
try {
|
||||
await updateUser(<User>{
|
||||
id: id,
|
||||
@ -89,11 +99,6 @@ export const actions: Actions = {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
return redirect(301, `/admin/users/${id}`);
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Missing username or password' });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
delete: async ({ cookies, params }) => {
|
||||
const id = parseInt(params.user!);
|
||||
|
||||
@ -100,7 +100,9 @@
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">Update User</div>
|
||||
<div class="p-3 font-semibold">
|
||||
Edit User {data.user.username}{data.user.fullName ? ` (${data.user.fullName})` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" class="px-4" autocomplete="off" use:enhance>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
@ -112,10 +114,11 @@
|
||||
value={data.user?.username}
|
||||
placeholder="Username"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="relative pt-4 text-sm font-semibold">
|
||||
Password
|
||||
New password (optional)
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
@ -134,7 +137,19 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email (optional)
|
||||
Full name <span class="danger-color">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
value={data.user?.fullName}
|
||||
placeholder="Full name"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email <span class="danger-color">*</span>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
@ -142,6 +157,7 @@
|
||||
value={data.user?.email}
|
||||
placeholder="Email"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
@ -156,17 +172,6 @@
|
||||
pattern="([0-9]\{3}) [0-9]\{3}-[0-9]\{3}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Full name (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
value={data.user?.fullName}
|
||||
placeholder="Full name"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Company code (optional)
|
||||
<input
|
||||
@ -195,7 +200,7 @@
|
||||
id="userPerms"
|
||||
class="select-all"
|
||||
checked={(perms & userPerms) === userPerms}
|
||||
indeterminate={(perms & userPerms) !== userPerms && (perms & userPerms) !== 0}
|
||||
indeterminate={(perms & userPerms) !== userPerms && (perms & userPerms) > 0}
|
||||
/></span
|
||||
>User Permissions
|
||||
</span>
|
||||
@ -212,7 +217,7 @@
|
||||
name="view"
|
||||
id="view"
|
||||
class="permCheckbox mx-1"
|
||||
checked={(perms & PERMISSIONS.VIEW) !== 0}
|
||||
checked={(perms & PERMISSIONS.VIEW) > 0}
|
||||
/>
|
||||
<span class="ml-2">View access</span></label
|
||||
>
|
||||
@ -224,7 +229,7 @@
|
||||
name="apply"
|
||||
id="apply"
|
||||
class="permCheckbox mx-1"
|
||||
checked={(perms & PERMISSIONS.APPLY_FOR_JOBS) !== 0}
|
||||
checked={(perms & PERMISSIONS.APPLY_FOR_JOBS) > 0}
|
||||
/>
|
||||
<span class="ml-2">Apply for jobs</span></label
|
||||
>
|
||||
@ -246,7 +251,7 @@
|
||||
class="select-all"
|
||||
checked={(perms & employerPerms) === employerPerms}
|
||||
indeterminate={(perms & employerPerms) !== employerPerms &&
|
||||
(perms & employerPerms) !== 0}
|
||||
(perms & employerPerms) > 0}
|
||||
/></span
|
||||
>Company Permissions
|
||||
</span>
|
||||
@ -262,7 +267,7 @@
|
||||
type="checkbox"
|
||||
name="submitPostings"
|
||||
id="submitPostings"
|
||||
checked={(perms & PERMISSIONS.SUBMIT_POSTINGS) !== 0}
|
||||
checked={(perms & PERMISSIONS.SUBMIT_POSTINGS) >= 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Submit postings</span></label
|
||||
@ -274,7 +279,7 @@
|
||||
type="checkbox"
|
||||
name="manageEmployers"
|
||||
id="manageEmployers"
|
||||
checked={(perms & PERMISSIONS.MANAGE_EMPLOYERS) !== 0}
|
||||
checked={(perms & PERMISSIONS.MANAGE_EMPLOYERS) > 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage employers (within their company)</span></label
|
||||
@ -296,7 +301,7 @@
|
||||
id="adminPerms"
|
||||
class="select-all"
|
||||
checked={(perms & adminPerms) === adminPerms}
|
||||
indeterminate={(perms & adminPerms) !== adminPerms && (perms & adminPerms) !== 0}
|
||||
indeterminate={(perms & adminPerms) !== adminPerms && (perms & adminPerms) > 0}
|
||||
/></span
|
||||
>Admin Permissions
|
||||
</span>
|
||||
@ -312,7 +317,7 @@
|
||||
type="checkbox"
|
||||
name="manageTags"
|
||||
id="manageTags"
|
||||
checked={(perms & PERMISSIONS.MANAGE_TAGS) !== 0}
|
||||
checked={(perms & PERMISSIONS.MANAGE_TAGS) > 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage tags</span></label
|
||||
@ -324,7 +329,7 @@
|
||||
type="checkbox"
|
||||
name="managePostings"
|
||||
id="managePostings"
|
||||
checked={(perms & PERMISSIONS.MANAGE_POSTINGS) !== 0}
|
||||
checked={(perms & PERMISSIONS.MANAGE_POSTINGS) > 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage postings</span></label
|
||||
@ -336,7 +341,7 @@
|
||||
type="checkbox"
|
||||
name="manageUsers"
|
||||
id="manageUsers"
|
||||
checked={(perms & PERMISSIONS.MANAGE_USERS) !== 0}
|
||||
checked={(perms & PERMISSIONS.MANAGE_USERS) > 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage users</span></label
|
||||
@ -348,7 +353,7 @@
|
||||
type="checkbox"
|
||||
name="manageCompanies"
|
||||
id="manageCompanies"
|
||||
checked={(perms & PERMISSIONS.MANAGE_COMPANIES) !== 0}
|
||||
checked={(perms & PERMISSIONS.MANAGE_COMPANIES) > 0}
|
||||
class="permCheckbox mx-1"
|
||||
/>
|
||||
<span class="ml-2">Manage companies</span></label
|
||||
@ -375,7 +380,7 @@
|
||||
<button
|
||||
class="dull-primary-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
type="submit"
|
||||
formaction="?/submit">Update user</button
|
||||
formaction="?/submit">Save user</button
|
||||
>
|
||||
<button
|
||||
class="danger-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
|
||||
@ -2,6 +2,8 @@ import { type Actions, fail, redirect } from '@sveltejs/kit';
|
||||
import { createUser } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { getUserPerms } from '$lib/index.server';
|
||||
import type { User } from '$lib/types';
|
||||
import { employerPerms, userPerms } from '$lib/shared.svelte';
|
||||
|
||||
export const actions: Actions = {
|
||||
submit: async ({ request, cookies }) => {
|
||||
@ -25,9 +27,7 @@ export const actions: Actions = {
|
||||
.toUpperCase()
|
||||
.trim();
|
||||
|
||||
if (email === '' || email == undefined) email = null;
|
||||
if (phone === '' || phone == undefined) phone = null;
|
||||
if (fullName === '' || fullName == undefined) fullName = null;
|
||||
if (companyCode === '' || companyCode == undefined) companyCode = null;
|
||||
|
||||
if (email && !email.includes('@')) {
|
||||
@ -51,16 +51,29 @@ export const actions: Actions = {
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (!(requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_USERS) > 0)) {
|
||||
return fail(403, { errorMessage: 'You cannot preform this action!' });
|
||||
} else {
|
||||
if ((requestPerms & newUserPerms) !== newUserPerms) {
|
||||
}
|
||||
if (((requestPerms | userPerms | employerPerms) & newUserPerms) !== newUserPerms) {
|
||||
return fail(403, {
|
||||
errorMessage: 'Cannot create a user with higher permissions than yourself!'
|
||||
});
|
||||
} else {
|
||||
if (username && password && username !== '' && password !== '') {
|
||||
}
|
||||
if (
|
||||
!username ||
|
||||
username === '' ||
|
||||
!password ||
|
||||
!email ||
|
||||
email === '' ||
|
||||
!fullName ||
|
||||
fullName === ''
|
||||
) {
|
||||
return fail(400, { errorMessage: 'Please fill out all required fields' });
|
||||
}
|
||||
if (password.length < 8) {
|
||||
return fail(400, { errorMessage: 'Password must be at least 8 characters' });
|
||||
}
|
||||
if (username.length < 4) {
|
||||
return fail(400, { errorMessage: 'Username must be at least 4 characters' });
|
||||
}
|
||||
let id = -1;
|
||||
try {
|
||||
id = await createUser(<User>{
|
||||
@ -79,10 +92,5 @@ export const actions: Actions = {
|
||||
if (id !== -1) {
|
||||
return redirect(301, `/admin/users/${id}`);
|
||||
}
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Missing username or password' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -118,13 +118,25 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email (optional)
|
||||
Full name <span class="danger-color">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
placeholder="Full Name"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email <span class="danger-color">*</span>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="Email"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
@ -138,16 +150,6 @@
|
||||
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Full name (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
placeholder="Full Name"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Company code (optional)
|
||||
<input
|
||||
|
||||
8
src/routes/api/posting/+server.ts
Normal file
8
src/routes/api/posting/+server.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { getPosting, getPostingFullData } from '$lib/db/index.server';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url }) {
|
||||
const id = url.searchParams.get('id');
|
||||
if (!id) return new Response(error(400, 'No id provided'));
|
||||
return json(await getPostingFullData(parseInt(id)));
|
||||
}
|
||||
6
src/routes/companies/+page.server.ts
Normal file
6
src/routes/companies/+page.server.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getCompanies } from '$lib/db/index.server';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
return { companies: await getCompanies() };
|
||||
};
|
||||
35
src/routes/companies/+page.svelte
Normal file
35
src/routes/companies/+page.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import type { Company, User } from '$lib/types';
|
||||
|
||||
function logoFallback(e: Event, company: Company) {
|
||||
(e.target as HTMLImageElement).src =
|
||||
`https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(company.name!)}`;
|
||||
}
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="base-container-small">
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders mb-4 mt-4 rounded">
|
||||
<div class="p-3 font-semibold">Companies</div>
|
||||
{#each data.companies as company}
|
||||
<a class="top-border hover-bg-color inline-block w-full p-3" href="/companies/{company.id}">
|
||||
<img
|
||||
class="mb-2 inline-block rounded-lg"
|
||||
src="/uploads/logos/{company.id}.svg?timestamp=${Date.now()}"
|
||||
alt="Company logo"
|
||||
onerror={(e) => logoFallback(e, company)}
|
||||
height="64"
|
||||
width="64"
|
||||
/>
|
||||
<div class="inline-block h-min pl-4 align-top">
|
||||
<h2 class="font-bold">{company.name}</h2>
|
||||
<p class="max-char-length">{company.description}</p>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
7
src/routes/companies/[company]/+page.server.ts
Normal file
7
src/routes/companies/[company]/+page.server.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getCompanyFullData } from '$lib/db/index.server';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const id = parseInt(params.company!);
|
||||
return await getCompanyFullData(id);
|
||||
};
|
||||
109
src/routes/companies/[company]/+page.svelte
Normal file
109
src/routes/companies/[company]/+page.svelte
Normal file
@ -0,0 +1,109 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import { userState } from '$lib/shared.svelte';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import type { User } from '$lib/types';
|
||||
|
||||
function avatarFallback(e: Event, user: User): null {
|
||||
(e.target as HTMLImageElement).src =
|
||||
`https://ui-avatars.com/api/?background=random&format=svg&name=${user.fullName ? encodeURIComponent(user.fullName) : encodeURIComponent(user.username)}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
function logoFallback(e: Event) {
|
||||
(e.target as HTMLImageElement).src =
|
||||
`https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(data.company.name!)}`;
|
||||
}
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="base-container">
|
||||
<div class="content py-4">
|
||||
<div class="bottom-border mb-4 flex justify-between">
|
||||
<div class="inline-block">
|
||||
<img
|
||||
class="mb-2 inline-block rounded-lg"
|
||||
src="/uploads/logos/{data.company.id}.svg?timestamp=${Date.now()}"
|
||||
alt="User avatar"
|
||||
onerror={logoFallback}
|
||||
height="120"
|
||||
width="120"
|
||||
/>
|
||||
<div class="inline-block h-min pl-4">
|
||||
<h1 class="font-bold">{data.company.name}</h1>
|
||||
<p>{data.company.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if (userState.perms & PERMISSIONS.MANAGE_COMPANIES) > 0 || ((userState.perms & PERMISSIONS.MANAGE_EMPLOYERS) !== 0 && userState.companyId === data.company.id)}
|
||||
<div class="inline-block">
|
||||
<a
|
||||
class="dull-primary-bg-color rounded px-3 py-1.5"
|
||||
href="/companies/{data.company.id}/edit">Edit company</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="elevated separator-borders mr-4 inline-block h-min w-3/4 rounded">
|
||||
<div class="p-3 font-semibold">
|
||||
{data.postings
|
||||
? data.company.name + "'s Postings"
|
||||
: data.company.name + ' has no current postings!'}
|
||||
</div>
|
||||
{#each data.postings as posting}
|
||||
<a class="top-border hover-bg-color block p-2" href="/postings/{posting.id}">
|
||||
<img
|
||||
class="inline-block rounded"
|
||||
src="/uploads/logos/{posting.companyId ? posting.companyId : 'default'}.svg"
|
||||
alt="Company Logo"
|
||||
height="48"
|
||||
width="48"
|
||||
onerror={logoFallback}
|
||||
/>
|
||||
<div class="inline-block pl-2 align-top">
|
||||
<h2 class="font-semibold">{posting.title}</h2>
|
||||
<h3 class="max-char-length">{posting.description}</h3>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="elevated separator-borders inline-block h-min w-1/4 rounded">
|
||||
<div class="flex place-content-between">
|
||||
<div class="p-3 font-semibold">Employers</div>
|
||||
</div>
|
||||
{#each data.users as user}
|
||||
<div class="top-border px-3 pb-2 pt-3">
|
||||
<div class="flex">
|
||||
<img
|
||||
class="mb-2 inline-block h-min rounded"
|
||||
src="/uploads/avatars/{data.company.id}.svg?timestamp=${Date.now()}"
|
||||
alt="User avatar"
|
||||
onerror={(e) => avatarFallback(e, user)}
|
||||
height="32"
|
||||
width="32"
|
||||
/>
|
||||
<div class="pl-2">
|
||||
<div class="pb-1 font-semibold">
|
||||
{user.username}{user.fullName ? ` (${user.fullName})` : ''}
|
||||
</div>
|
||||
{#if user.email}
|
||||
<div class="pb-1">
|
||||
<span class="material-symbols-outlined align-middle">mail</span>
|
||||
<a class="hover-hyperlink" href="mailto:{user.email}">{user.email}</a>
|
||||
</div>
|
||||
{/if}
|
||||
{#if user.phone}
|
||||
<div class="pb-1">
|
||||
<span class="material-symbols-outlined align-middle">call</span>
|
||||
<a class="hover-hyperlink" href="tel:{user.phone}">{user.phone}</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
27
src/routes/companies/[company]/edit/+layout.svelte
Normal file
27
src/routes/companies/[company]/edit/+layout.svelte
Normal file
@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import type { LayoutProps } from './$types';
|
||||
|
||||
let { data, children }: LayoutProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="bottom-border h-10 pt-2 text-center">
|
||||
<a
|
||||
href={page.url.pathname.endsWith('employers') ? '.' : ''}
|
||||
class="p-2 {page.url.pathname.endsWith('edit')
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text low-emphasis-text-button'}"
|
||||
><span class="material-symbols-outlined align-bottom">store</span> Details</a
|
||||
>
|
||||
<a
|
||||
href={page.url.pathname.endsWith('edit') ? 'edit/employers' : ''}
|
||||
class="p-2 {page.url.pathname.endsWith('employers')
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text low-emphasis-text-button'}"
|
||||
><span class="material-symbols-outlined align-bottom">group</span> Employers</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="base-container">
|
||||
{@render children()}
|
||||
</div>
|
||||
5
src/routes/companies/[company]/edit/+layout.ts
Normal file
5
src/routes/companies/[company]/edit/+layout.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async ({ params }) => {
|
||||
return { id: parseInt(params.company) };
|
||||
};
|
||||
70
src/routes/companies/[company]/edit/+page.server.ts
Normal file
70
src/routes/companies/[company]/edit/+page.server.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { type Actions, error, fail, redirect } from '@sveltejs/kit';
|
||||
import { deleteCompany, editCompany, getCompany } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { getUserCompanyId, getUserPerms } from '$lib/index.server';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import type { Company } from '$lib/types';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, params }) => {
|
||||
const id = parseInt(params.company);
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_COMPANIES) > 0) {
|
||||
return {
|
||||
company: await getCompany(id)
|
||||
};
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
submit: async ({ request, cookies, params }) => {
|
||||
const id = parseInt(params.company!);
|
||||
const data = await request.formData();
|
||||
const name = data.get('name')?.toString().trim();
|
||||
let website = data.get('website')?.toString().trim();
|
||||
const description = data.get('description')?.toString().trim();
|
||||
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (
|
||||
!(
|
||||
requestPerms >= 0 &&
|
||||
((requestPerms & PERMISSIONS.MANAGE_COMPANIES) > 0 ||
|
||||
((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id))
|
||||
)
|
||||
) {
|
||||
return fail(403, { errorMessage: 'You cannot preform this action!' });
|
||||
}
|
||||
if (!name || name === '' || !website || website === '' || !description || description === '') {
|
||||
return fail(400, { errorMessage: 'All fields are required' });
|
||||
}
|
||||
if (!website.includes('.')) {
|
||||
return fail(400, { errorMessage: 'Invalid website' });
|
||||
}
|
||||
if (!website.startsWith('http://') && !website.startsWith('https://'))
|
||||
website = `https://${website}`;
|
||||
|
||||
try {
|
||||
await editCompany(<Company>{
|
||||
id: id,
|
||||
name: name,
|
||||
website: website,
|
||||
description: description
|
||||
});
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
redirect(301, `/companies/${id}`);
|
||||
},
|
||||
delete: async ({ cookies, params }) => {
|
||||
const id = parseInt(params.company!);
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (!(requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_COMPANIES) > 0)) {
|
||||
return fail(403, { errorMessage: 'You cannot preform this action!' });
|
||||
}
|
||||
try {
|
||||
await deleteCompany(id);
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
}
|
||||
};
|
||||
108
src/routes/companies/[company]/edit/+page.svelte
Normal file
108
src/routes/companies/[company]/edit/+page.svelte
Normal file
@ -0,0 +1,108 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
function openConfirm() {
|
||||
document.getElementById('deleteConfirmModal')!.style.display = 'block';
|
||||
}
|
||||
|
||||
function closeConfirm() {
|
||||
document.getElementById('deleteConfirmModal')!.style.display = 'none';
|
||||
}
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="base-container">
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">Edit Company {data.company.name}</div>
|
||||
</div>
|
||||
<form method="POST" class="px-4" autocomplete="off" use:enhance>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Name <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Name"
|
||||
value={data.company?.name}
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Description <span class="text-red-500">*</span>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
rows="4"
|
||||
placeholder="Description"
|
||||
class="w-full rounded font-normal">{data.company?.description}</textarea
|
||||
>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Website <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
id="website"
|
||||
placeholder="Website"
|
||||
value={data.company?.website}
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if form?.errorMessage}
|
||||
<div class="mb-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
<div class="mt-4 flex justify-between">
|
||||
<button
|
||||
class="dull-primary-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
type="submit"
|
||||
formaction="?/submit">Save company</button
|
||||
>
|
||||
<button
|
||||
class="danger-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
type="button"
|
||||
onclick={openConfirm}>Delete company</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<form id="deleteConfirmModal" class="modal" method="POST" use:enhance>
|
||||
<div class="modal-content">
|
||||
<div class="mb-2 inline-flex w-full justify-between">
|
||||
<h2 class="font-semibold">Are you sure?</h2>
|
||||
<button class="material-symbols-outlined" onclick={closeConfirm}>close</button>
|
||||
</div>
|
||||
<p>
|
||||
This will permanently delete company <span class="font-semibold"
|
||||
>{data.company?.name}.</span
|
||||
>
|
||||
</p>
|
||||
<p>Please type "I understand" into the box below to confirm</p>
|
||||
<input
|
||||
type="text"
|
||||
name="confirm"
|
||||
id="confirm"
|
||||
placeholder="I understand"
|
||||
class="w-full rounded font-normal"
|
||||
pattern="I understand"
|
||||
required
|
||||
/>
|
||||
<div class="mt-4 flex justify-between">
|
||||
<button class="danger-bg-color rounded px-2 py-1" type="submit" formaction="?/delete"
|
||||
>Delete company</button
|
||||
>
|
||||
<button
|
||||
class="separator-borders bg-color rounded px-2 py-1"
|
||||
type="button"
|
||||
onclick={closeConfirm}>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,56 @@
|
||||
import { type Actions, error, fail, redirect } from '@sveltejs/kit';
|
||||
import {
|
||||
addEmployerToCompany,
|
||||
deleteCompany,
|
||||
editCompany,
|
||||
getCompany,
|
||||
getCompanyEmployers,
|
||||
removeEmployerFromCompany
|
||||
} from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { getUserCompanyId, getUserPerms } from '$lib/index.server';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, params }) => {
|
||||
const id = parseInt(params.company);
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_COMPANIES) > 0) {
|
||||
return await getCompanyEmployers(id);
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
removeEmployer: async ({ params, url, cookies }) => {
|
||||
const id = parseInt(params.company!);
|
||||
const data = url.searchParams;
|
||||
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (
|
||||
!(
|
||||
(requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_POSTINGS) > 0) ||
|
||||
((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id)
|
||||
)
|
||||
) {
|
||||
return fail(403, { errorMessage: 'You cannot preform this action!' });
|
||||
}
|
||||
|
||||
await removeEmployerFromCompany(id, parseInt(data.get('userId')!));
|
||||
},
|
||||
addEmployer: async ({ params, url, cookies }) => {
|
||||
const id = parseInt(params.company!);
|
||||
const data = url.searchParams;
|
||||
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (
|
||||
!(
|
||||
(requestPerms >= 0 && (requestPerms & PERMISSIONS.MANAGE_POSTINGS) > 0) ||
|
||||
((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id)
|
||||
)
|
||||
) {
|
||||
return fail(403, { errorMessage: 'You cannot preform this action!' });
|
||||
}
|
||||
|
||||
await addEmployerToCompany(id, parseInt(data.get('userId')!));
|
||||
}
|
||||
};
|
||||
174
src/routes/companies/[company]/edit/employers/+page.svelte
Normal file
174
src/routes/companies/[company]/edit/employers/+page.svelte
Normal file
@ -0,0 +1,174 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
const dateFormatOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
};
|
||||
|
||||
function logoFallback(e: Event) {
|
||||
(e.target as HTMLImageElement).src =
|
||||
`https://ui-avatars.com/api/?background=random&format=svg&name=${encodeURIComponent(data.company.name!)}`;
|
||||
}
|
||||
|
||||
let idToRemove: number | null = $state(null);
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="m-4">
|
||||
<img
|
||||
class="mb-2 inline-block rounded-lg"
|
||||
src="/uploads/logos/{data.company.id}.svg?timestamp=${Date.now()}"
|
||||
alt="User avatar"
|
||||
onerror={logoFallback}
|
||||
height="80"
|
||||
width="80"
|
||||
/>
|
||||
<div class="inline-block pl-4 align-top">
|
||||
<h1 class="font-bold">{data.company.name}</h1>
|
||||
<h2>Company code: <span class="font-semibold">{data.company.companyCode}</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">Current Employers</div>
|
||||
</div>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left w-16 py-1">ID</th>
|
||||
<th class="py-1">Username</th>
|
||||
<th class="py-1">Full Name</th>
|
||||
<th class="py-1">Email</th>
|
||||
<th class="py-1">Created</th>
|
||||
<th class="py-1">Last Sign-In</th>
|
||||
<th class="py-1">Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if data.users !== undefined}
|
||||
{#each data.users as user}
|
||||
{#if user.company?.id === data.id}
|
||||
<tr class="h-8">
|
||||
<td class="left">{user.id}</td>
|
||||
<td>{user.username}</td>
|
||||
<td>{user.fullName || 'N/A'}</td>
|
||||
<td>{user.email || 'N/A'}</td>
|
||||
<td
|
||||
>{user.createdAt?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td
|
||||
>{user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td class="material-symbols-outlined hover-bg-color danger-color m-1 rounded"
|
||||
><button
|
||||
onclick={() => {
|
||||
idToRemove = user.id;
|
||||
}}>close</button
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if idToRemove !== null}
|
||||
<form method="POST" id="deleteConfirmModal" class="modal-always-display">
|
||||
<div class="modal-content">
|
||||
<div class="mb-2 inline-flex w-full justify-between">
|
||||
<h2 class="font-semibold">Are you sure?</h2>
|
||||
<button
|
||||
class="material-symbols-outlined"
|
||||
onclick={() => {
|
||||
idToRemove = null;
|
||||
}}>close</button
|
||||
>
|
||||
</div>
|
||||
<p>This will remove this employer from the company.</p>
|
||||
<div class="mt-4 flex justify-between">
|
||||
<button
|
||||
class="danger-bg-color rounded px-2 py-1"
|
||||
type="submit"
|
||||
formaction="?/removeEmployer&userId={idToRemove}">Remove</button
|
||||
>
|
||||
<button
|
||||
class="separator-borders bg-color rounded px-2 py-1"
|
||||
type="button"
|
||||
onclick={() => {
|
||||
idToRemove = null;
|
||||
}}>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
{#if data.users && data.users.some((user) => {
|
||||
return user.company?.id !== data.id;
|
||||
})}
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">Pending requests</div>
|
||||
</div>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left w-16 py-1">ID</th>
|
||||
<th class="py-1">Username</th>
|
||||
<th class="py-1">Full Name</th>
|
||||
<th class="py-1">Email</th>
|
||||
<th class="py-1">Created</th>
|
||||
<th class="py-1">Last Sign-In</th>
|
||||
<th class="py-1">Approve</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if data.users !== undefined}
|
||||
{#each data.users as user}
|
||||
{#if user.company?.id !== data.id}
|
||||
<tr class="h-8">
|
||||
<td class="left">{user.id}</td>
|
||||
<td>{user.username}</td>
|
||||
<td>{user.fullName || 'N/A'}</td>
|
||||
<td>{user.email || 'N/A'}</td>
|
||||
<td
|
||||
>{user.createdAt?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td
|
||||
>{user.lastSignIn?.toLocaleDateString('en-US', dateFormatOptions) ||
|
||||
'unknown'}</td
|
||||
>
|
||||
<td class="material-symbols-outlined"
|
||||
><form method="POST" class="flex">
|
||||
<button
|
||||
class="hover-bg-color m-1 rounded text-green-600"
|
||||
formaction="?/addEmployer&userId={user.id}">check</button
|
||||
>
|
||||
<button
|
||||
class="hover-bg-color danger-color m-1 rounded"
|
||||
formaction="?/removeEmployer&userId={user.id}">close</button
|
||||
>
|
||||
</form></td
|
||||
>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
45
src/routes/companies/create/+page.server.ts
Normal file
45
src/routes/companies/create/+page.server.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { type Actions, fail, redirect } from '@sveltejs/kit';
|
||||
import { createCompany } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { getUserCompanyId, getUserPerms } from '$lib/index.server';
|
||||
import type { Company } from '$lib/types';
|
||||
|
||||
export const actions: Actions = {
|
||||
submit: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
const name = data.get('name')?.toString().trim();
|
||||
let website = data.get('website')?.toString().trim();
|
||||
const description = data.get('description')?.toString().trim();
|
||||
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (
|
||||
!(
|
||||
requestPerms >= 0 &&
|
||||
((requestPerms & PERMISSIONS.MANAGE_COMPANIES) > 0 ||
|
||||
((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies)))
|
||||
)
|
||||
) {
|
||||
return fail(403, { errorMessage: 'You cannot preform this action!' });
|
||||
}
|
||||
if (!name || name === '' || !website || website === '' || !description || description === '') {
|
||||
return fail(400, { errorMessage: 'All fields are required' });
|
||||
}
|
||||
if (!website.includes('.')) {
|
||||
return fail(400, { errorMessage: 'Invalid website' });
|
||||
}
|
||||
if (!website.startsWith('http://') && !website.startsWith('https://'))
|
||||
website = `https://${website}`;
|
||||
|
||||
let id: number;
|
||||
try {
|
||||
id = await createCompany(<Company>{
|
||||
name: name,
|
||||
website: website,
|
||||
description: description
|
||||
});
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
redirect(301, `/companies/${id}`);
|
||||
}
|
||||
};
|
||||
60
src/routes/companies/create/+page.svelte
Normal file
60
src/routes/companies/create/+page.svelte
Normal file
@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="base-container">
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">Create new company</div>
|
||||
</div>
|
||||
<form method="POST" class="px-4" autocomplete="off" use:enhance>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Name <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Name"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Description <span class="text-red-500">*</span>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
rows="4"
|
||||
placeholder="Description"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Website <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
id="website"
|
||||
placeholder="Website"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#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"
|
||||
type="submit"
|
||||
formaction="?/submit">Create company</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
23
src/routes/info/+page.svelte
Normal file
23
src/routes/info/+page.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<div class="base-container">
|
||||
<div class="content elevated separator-borders m-2 rounded p-2">
|
||||
<h1 class="font-bold">Info</h1>
|
||||
<p>This page contains additional info about different parts of the app</p>
|
||||
<h2 class="pt-2 font-semibold" id="company-codes">Company Codes</h2>
|
||||
<p>
|
||||
Company codes are unique identifiers of companies in order for an employer to associate
|
||||
themselves to a company.
|
||||
</p>
|
||||
<p>If you are an applicant, you can safely ignore them.</p>
|
||||
<p>
|
||||
If you are an employer, get a code from your company admin, and input it into your account
|
||||
page to request access to your company. Once approved by your administrator, you will have
|
||||
access to create job postings.
|
||||
</p>
|
||||
<p>
|
||||
If you are your company admin, first create your account (without inputting a code). Then, go
|
||||
to the company page, and use the button in the top right to create a new company. Once created
|
||||
and approved by a CareerConnect admin, you will be able to see the company code, which you can
|
||||
then give to your employees.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
8
src/routes/postings/+page.server.ts
Normal file
8
src/routes/postings/+page.server.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getPostings } from '$lib/db/index.server';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
return {
|
||||
postings: await getPostings(url.searchParams.get('searchQuery') as string)
|
||||
};
|
||||
};
|
||||
140
src/routes/postings/+page.svelte
Normal file
140
src/routes/postings/+page.svelte
Normal file
@ -0,0 +1,140 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import type { Posting } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { employmentTypeDisplayName, userState } from '$lib/shared.svelte';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
let details: Posting | undefined = $state<Posting>();
|
||||
|
||||
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')}`;
|
||||
}
|
||||
|
||||
async function fetchDetails(id: number) {
|
||||
const response = await fetch(`/api/posting?id=${id}`);
|
||||
details = await response.json();
|
||||
if (details?.createdAt) {
|
||||
details.createdAt = new Date(details.createdAt);
|
||||
}
|
||||
if (details?.updatedAt) {
|
||||
details.updatedAt = new Date(details.updatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
onMount(async () => {
|
||||
await fetchDetails(data.postings[0].id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="base-container">
|
||||
<div class="content flex">
|
||||
<div class="right-border inline-block w-1/3">
|
||||
{#each data.postings as posting}
|
||||
<button
|
||||
class="bottom-border block w-full p-4 text-left {details?.id === posting.id
|
||||
? 'accent-bg-color'
|
||||
: ''}"
|
||||
onclick={() => {
|
||||
fetchDetails(posting.id);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
class="inline-block rounded"
|
||||
src="/uploads/logos/{posting.companyId}.svg"
|
||||
alt="Company Logo"
|
||||
height="48"
|
||||
width="48"
|
||||
onerror={(e) => logoFallback(e, posting)}
|
||||
/>
|
||||
<div class="inline-block pl-2 align-top">
|
||||
<h2 class="font-semibold">{posting.title}</h2>
|
||||
<h3>{posting.company.name}</h3>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if details !== undefined}
|
||||
<div
|
||||
class="elevated separator-borders top-with-navbar sticky ml-4 mt-4 inline-block h-min w-2/3 rounded p-4"
|
||||
>
|
||||
<div class="bottom-border flex justify-between pb-2">
|
||||
<div class="inline-block">
|
||||
<img
|
||||
class="inline-block rounded"
|
||||
src="/uploads/logos/{details.companyId || 'default'}.svg"
|
||||
alt="Company Logo"
|
||||
height="64"
|
||||
width="64"
|
||||
onerror={(e) => logoFallback(e, details)}
|
||||
/>
|
||||
<div class="inline-block pl-2 align-top">
|
||||
<h1>{details.title}</h1>
|
||||
<h2>Company: {details.company.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{#if userState.perms >= 0 && ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0 || ((userState.perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && userState.companyId === details.company.id))}
|
||||
<a
|
||||
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
|
||||
>
|
||||
{:else if (userState.perms & PERMISSIONS.APPLY_FOR_JOBS) > 0}
|
||||
<a
|
||||
class="dull-primary-bg-color inline-block h-min rounded-md px-2.5 py-1 align-top"
|
||||
href="/postings/{details.id}/apply">Apply</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="scrollbar-on-elevated details-height overflow-y-scroll">
|
||||
<h2 class="pt-2 font-semibold">Contact</h2>
|
||||
<p>{details.employer?.fullName} ({details.employer?.username})</p>
|
||||
<a class="hover-hyperlink" href="mailto:{details.employer?.email}"
|
||||
>{details.employer?.email}</a
|
||||
>
|
||||
<a class="hover-hyperlink" href="tel:{details.employer?.phone}"
|
||||
>{details.employer?.phone}</a
|
||||
>
|
||||
<h2 class="pt-2 font-semibold">Details</h2>
|
||||
{#if details.employmentType}
|
||||
<p>{employmentTypeDisplayName(details.employmentType)}</p>
|
||||
{/if}
|
||||
{#if details.address}
|
||||
<a
|
||||
href="https://www.google.com/maps/search/?api=1&query={details.address}"
|
||||
class="block w-max">Address: <span class="hover-hyperlink">{details.address}</span></a
|
||||
>
|
||||
{/if}
|
||||
{#if details.wage}
|
||||
<p>Wage: {details.wage}</p>
|
||||
{/if}
|
||||
{#if details.createdAt}
|
||||
<p>Posted: {details.createdAt.toLocaleDateString('en-US', dateFormatOptions)}</p>
|
||||
{/if}
|
||||
{#if details.link}
|
||||
<a href={details.link} class="block w-max"
|
||||
>More information: <span class="hyperlink-color hyperlink-underline"
|
||||
>{details.link}</span
|
||||
></a
|
||||
>
|
||||
{/if}
|
||||
{#if details.flyerLink}
|
||||
<a href={details.flyerLink} class="block w-max"
|
||||
>Flyer: <span class="hyperlink-color hyperlink-underline">{details.flyerLink}</span
|
||||
></a
|
||||
>
|
||||
{/if}
|
||||
<h2 class="pt-2 font-semibold">Job Description</h2>
|
||||
<p class="whitespace-pre-wrap">{details.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
8
src/routes/postings/[posting]/+page.server.ts
Normal file
8
src/routes/postings/[posting]/+page.server.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getPostingFullData, getPostings } from '$lib/db/index.server';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
return {
|
||||
posting: await getPostingFullData(parseInt(params.posting))
|
||||
};
|
||||
};
|
||||
95
src/routes/postings/[posting]/+page.svelte
Normal file
95
src/routes/postings/[posting]/+page.svelte
Normal file
@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
import type { PageProps } from './$types';
|
||||
import type { Posting } from '$lib/types';
|
||||
import { employmentTypeDisplayName, userState } from '$lib/shared.svelte';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
|
||||
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 }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="base-container-small">
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders ml-4 mt-4 inline-block h-min w-full rounded p-4">
|
||||
<div class="bottom-border elevated-bg flex justify-between pb-2">
|
||||
<div class="inline-block">
|
||||
<img
|
||||
class="inline-block rounded"
|
||||
src="/uploads/logos/{data.posting.companyId || 'default'}.svg"
|
||||
alt="Company Logo"
|
||||
height="64"
|
||||
width="64"
|
||||
onerror={(e) => logoFallback(e, data.posting)}
|
||||
/>
|
||||
<div class="inline-block pl-2 align-top">
|
||||
<h1>{data.posting.title}</h1>
|
||||
<h2>Company: {data.posting.company.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{#if userState.perms >= 0 && ((userState.perms & PERMISSIONS.MANAGE_POSTINGS) > 0 || ((userState.perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && userState.companyId === details.company.id))}
|
||||
<a
|
||||
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
|
||||
>
|
||||
{:else if (userState.perms & PERMISSIONS.APPLY_FOR_JOBS) > 0}
|
||||
<a
|
||||
class="dull-primary-bg-color inline-block h-min rounded-md px-2.5 py-1 align-top"
|
||||
href="/postings/{data.posting.id}/apply">Apply</a
|
||||
>
|
||||
{/if}
|
||||
</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>
|
||||
</div>
|
||||
37
src/routes/postings/[posting]/apply/+page.server.ts
Normal file
37
src/routes/postings/[posting]/apply/+page.server.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { type Actions, error, fail, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { createApplication, getPostingFullData, getUserWithCompany } 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 ({ params, cookies }) => {
|
||||
const id = parseInt(params.posting);
|
||||
const perms = getUserPerms(cookies);
|
||||
if (perms >= 0 && (perms & PERMISSIONS.MANAGE_USERS) > 0) {
|
||||
return {
|
||||
posting: await getPostingFullData(id)
|
||||
};
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
submit: async ({ request, cookies, params }) => {
|
||||
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();
|
||||
|
||||
const postingId = parseInt(params.posting!);
|
||||
const userId = getUserId(cookies);
|
||||
|
||||
if (!candidateStatement || candidateStatement === '') {
|
||||
return fail(400, { errorMessage: 'Candidate statement is required' });
|
||||
}
|
||||
await createApplication(<Application>{ userId, postingId, candidateStatement });
|
||||
redirect(301, `/postings`);
|
||||
}
|
||||
};
|
||||
116
src/routes/postings/[posting]/apply/+page.svelte
Normal file
116
src/routes/postings/[posting]/apply/+page.svelte
Normal file
@ -0,0 +1,116 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import { employmentTypeDisplayName, userState } from '$lib/shared.svelte';
|
||||
import type { Posting } from '$lib/types';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
|
||||
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
|
||||
class="inline-block rounded"
|
||||
src="/uploads/logos/{data.posting?.companyId || 'default'}.svg"
|
||||
alt="Company Logo"
|
||||
height="64"
|
||||
width="64"
|
||||
onerror={(e) => logoFallback(e, data.posting)}
|
||||
/>
|
||||
<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">Apply</div>
|
||||
</div>
|
||||
<form method="POST" class="px-4" autocomplete="off" 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
|
||||
name="candidateStatement"
|
||||
id="candidateStatement"
|
||||
rows="4"
|
||||
placeholder="Answer here"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
<p>
|
||||
Your account information 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"
|
||||
type="submit"
|
||||
formaction="?/submit">Submit application</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
27
src/routes/postings/[posting]/manage/+layout.svelte
Normal file
27
src/routes/postings/[posting]/manage/+layout.svelte
Normal file
@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import type { LayoutProps } from './$types';
|
||||
|
||||
let { children }: LayoutProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="bottom-border h-10 pt-2 text-center">
|
||||
<a
|
||||
href="applications"
|
||||
class="p-2 {page.url.pathname.endsWith('applications')
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text low-emphasis-text-button'}"
|
||||
><span class="material-symbols-outlined align-bottom">description</span> Applications</a
|
||||
>
|
||||
<a
|
||||
href="edit"
|
||||
class="p-2 {page.url.pathname.endsWith('edit')
|
||||
? 'primary-underline font-bold'
|
||||
: 'low-emphasis-text low-emphasis-text-button'}"
|
||||
><span class="material-symbols-outlined align-bottom">work</span> Details</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="base-container">
|
||||
{@render children()}
|
||||
</div>
|
||||
7
src/routes/postings/[posting]/manage/+page.svelte
Normal file
7
src/routes/postings/[posting]/manage/+page.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
window.location.href = 'manage/applications';
|
||||
});
|
||||
</script>
|
||||
123
src/routes/postings/[posting]/manage/edit/+page.server.ts
Normal file
123
src/routes/postings/[posting]/manage/edit/+page.server.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { type Actions, error, fail, redirect } from '@sveltejs/kit';
|
||||
import { deletePosting, editPosting, getPosting, getPostings, getUser } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { getUserCompanyId, getUserId, getUserPerms } from '$lib/index.server';
|
||||
import type { Posting } from '$lib/types';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, params }) => {
|
||||
const id = parseInt(params.posting);
|
||||
const perms = getUserPerms(cookies);
|
||||
|
||||
if (
|
||||
perms >= 0 &&
|
||||
((perms & PERMISSIONS.MANAGE_POSTINGS) > 0 ||
|
||||
((perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id))
|
||||
) {
|
||||
return {
|
||||
posting: await getPosting(id)
|
||||
};
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
submit: async ({ request, cookies, params }) => {
|
||||
const id = parseInt(params.posting!);
|
||||
const employerId = getUserId(cookies);
|
||||
|
||||
const data = await request.formData();
|
||||
let companyId: number | null | undefined = getUserCompanyId(cookies);
|
||||
if (!companyId) {
|
||||
if (
|
||||
data.get('companyId')?.toString().trim() === undefined ||
|
||||
data.get('companyId')?.toString().trim() === null ||
|
||||
data.get('companyId')?.toString().trim() === ''
|
||||
) {
|
||||
return fail(400, { errorMessage: 'Company ID is required' });
|
||||
} else {
|
||||
companyId = parseInt(data.get('companyId')?.toString().trim()!);
|
||||
}
|
||||
}
|
||||
|
||||
const title = data.get('title')?.toString().trim();
|
||||
const description = data.get('description')?.toString().trim();
|
||||
const address = data.get('address')?.toString().trim();
|
||||
const employmentType = data.get('employmentType')?.toString().trim();
|
||||
const wage = data.get('wage')?.toString().trim();
|
||||
let link = data.get('link')?.toString().trim();
|
||||
const tagIds = data.get('tagIds')?.toString().trim() || null;
|
||||
let flyerLink = data.get('flyerLink')?.toString().trim();
|
||||
|
||||
if (link && !link?.includes('.')) {
|
||||
return fail(400, { errorMessage: 'Invalid link' });
|
||||
}
|
||||
if (flyerLink && !flyerLink?.includes('.')) {
|
||||
return fail(400, { errorMessage: 'Invalid flyer link' });
|
||||
}
|
||||
if (link && !link.startsWith('http://') && !link.startsWith('https://'))
|
||||
link = `https://${link}`;
|
||||
if (flyerLink && !flyerLink.startsWith('http://') && !flyerLink.startsWith('https://'))
|
||||
flyerLink = `https://${flyerLink}`;
|
||||
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (
|
||||
!(
|
||||
requestPerms >= 0 &&
|
||||
((requestPerms & PERMISSIONS.MANAGE_POSTINGS) > 0 ||
|
||||
((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 &&
|
||||
getUserCompanyId(cookies) === employerId))
|
||||
)
|
||||
) {
|
||||
return fail(403, { errorMessage: 'You cannot preform this action!' });
|
||||
}
|
||||
if (
|
||||
!title ||
|
||||
title === '' ||
|
||||
!description ||
|
||||
description === '' ||
|
||||
!address ||
|
||||
address === '' ||
|
||||
!employmentType ||
|
||||
employmentType === ''
|
||||
) {
|
||||
return fail(400, { errorMessage: 'All fields are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
await editPosting(<Posting>{
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
employerId,
|
||||
address,
|
||||
employmentType,
|
||||
wage,
|
||||
link,
|
||||
tagIds: tagIds?.split(',').map((tag) => parseInt(tag)),
|
||||
companyId,
|
||||
flyerLink
|
||||
});
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
redirect(301, `/postings/${id}`);
|
||||
},
|
||||
delete: async ({ cookies, params }) => {
|
||||
const id = parseInt(params.posting!);
|
||||
const perms = getUserPerms(cookies);
|
||||
if (
|
||||
perms >= 0 &&
|
||||
((perms & PERMISSIONS.MANAGE_POSTINGS) > 0 ||
|
||||
((perms & PERMISSIONS.SUBMIT_POSTINGS) > 0 && getUserCompanyId(cookies) === id))
|
||||
) {
|
||||
try {
|
||||
await deletePosting(id);
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
redirect(301, '/postings');
|
||||
}
|
||||
error(403, 'Unauthorized');
|
||||
}
|
||||
};
|
||||
160
src/routes/postings/[posting]/manage/edit/+page.svelte
Normal file
160
src/routes/postings/[posting]/manage/edit/+page.svelte
Normal file
@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import { userState } from '$lib/shared.svelte.js';
|
||||
|
||||
function openConfirm() {
|
||||
document.getElementById('deleteConfirmModal')!.style.display = 'block';
|
||||
}
|
||||
|
||||
function closeConfirm() {
|
||||
document.getElementById('deleteConfirmModal')!.style.display = 'none';
|
||||
}
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="base-container">
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">Edit {data.posting.title}</div>
|
||||
</div>
|
||||
<form method="POST" class="px-4" autocomplete="off" use:enhance>
|
||||
{#if !userState.companyId}
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Company ID <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="number"
|
||||
name="companyId"
|
||||
id="companyId"
|
||||
placeholder="Company ID"
|
||||
value={data.posting.companyId}
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Title <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder="Title"
|
||||
value={data.posting.title}
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Description <span class="text-red-500">*</span>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
rows="4"
|
||||
placeholder="Description"
|
||||
class="w-full rounded font-normal"
|
||||
required>{data.posting.description}</textarea
|
||||
>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Address <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
id="address"
|
||||
placeholder="Address"
|
||||
value={data.posting.address}
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
<label for="employmentType">Employment type <span class="text-red-500">*</span></label>
|
||||
<select
|
||||
name="employmentType"
|
||||
id="employmentType"
|
||||
value={data.posting.employmentType}
|
||||
class="w-full rounded font-normal"
|
||||
>
|
||||
<option value="full_time">Full time</option>
|
||||
<option value="part_time">Part time</option>
|
||||
<option value="internship">Internship</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Wage (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="wage"
|
||||
id="wage"
|
||||
placeholder="Wage"
|
||||
value={data.posting.wage}
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Link to external posting information (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="link"
|
||||
id="link"
|
||||
placeholder="Link"
|
||||
value={data.posting.link}
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Link to flyer (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="flyerLink"
|
||||
id="flyerLink"
|
||||
placeholder="Flyer link"
|
||||
value={data.posting.flyerLink}
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if form?.errorMessage}
|
||||
<div class="mb-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
<div class="mt-4 flex justify-between">
|
||||
<button
|
||||
class="dull-primary-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
type="submit"
|
||||
formaction="?/submit">Save posting</button
|
||||
>
|
||||
<button
|
||||
class="danger-bg-color mb-4 mt-2 rounded px-2 py-1"
|
||||
type="button"
|
||||
onclick={openConfirm}>Delete posting</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form id="deleteConfirmModal" method="POST" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="mb-2 inline-flex w-full justify-between">
|
||||
<h2 class="font-semibold">Are you sure?</h2>
|
||||
<button class="material-symbols-outlined" onclick={closeConfirm}>close</button>
|
||||
</div>
|
||||
<p>This will permanently delete this posting. This action cannot be undone.</p>
|
||||
|
||||
<div class="mt-4 flex justify-between">
|
||||
<button
|
||||
class="danger-bg-color rounded px-2 py-1"
|
||||
type="submit"
|
||||
formaction="?/delete&id={data.posting.id}">Delete posting</button
|
||||
>
|
||||
<button
|
||||
class="separator-borders bg-color rounded px-2 py-1"
|
||||
type="button"
|
||||
onclick={closeConfirm}>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
90
src/routes/postings/create/+page.server.ts
Normal file
90
src/routes/postings/create/+page.server.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { type Actions, fail, redirect } from '@sveltejs/kit';
|
||||
import { createPosting } from '$lib/db/index.server';
|
||||
import { PERMISSIONS } from '$lib/consts';
|
||||
import { getUserCompanyId, getUserId, getUserPerms } from '$lib/index.server';
|
||||
import type { Posting } from '$lib/types';
|
||||
|
||||
export const actions: Actions = {
|
||||
submit: async ({ request, cookies }) => {
|
||||
const employerId = getUserId(cookies);
|
||||
|
||||
const data = await request.formData();
|
||||
let companyId: number | null | undefined = getUserCompanyId(cookies);
|
||||
if (!companyId) {
|
||||
if (
|
||||
data.get('companyId')?.toString().trim() === undefined ||
|
||||
data.get('companyId')?.toString().trim() === null ||
|
||||
data.get('companyId')?.toString().trim() === ''
|
||||
) {
|
||||
return fail(400, { errorMessage: 'Company ID is required' });
|
||||
} else {
|
||||
companyId = parseInt(data.get('companyId')?.toString().trim()!);
|
||||
}
|
||||
}
|
||||
|
||||
const title = data.get('title')?.toString().trim();
|
||||
const description = data.get('description')?.toString().trim();
|
||||
const address = data.get('address')?.toString().trim();
|
||||
const employmentType = data.get('employmentType')?.toString().trim();
|
||||
const wage = data.get('wage')?.toString().trim();
|
||||
let link = data.get('link')?.toString().trim();
|
||||
const tagIds = data.get('tagIds')?.toString().trim() || null;
|
||||
let flyerLink = data.get('flyerLink')?.toString().trim();
|
||||
|
||||
if (link && !link?.includes('.')) {
|
||||
return fail(400, { errorMessage: 'Invalid link' });
|
||||
}
|
||||
if (flyerLink && !flyerLink?.includes('.')) {
|
||||
return fail(400, { errorMessage: 'Invalid flyer link' });
|
||||
}
|
||||
if (link && !link.startsWith('http://') && !link.startsWith('https://'))
|
||||
link = `https://${link}`;
|
||||
if (flyerLink && !flyerLink.startsWith('http://') && !flyerLink.startsWith('https://'))
|
||||
flyerLink = `https://${flyerLink}`;
|
||||
|
||||
const requestPerms = getUserPerms(cookies);
|
||||
if (
|
||||
!(
|
||||
requestPerms >= 0 &&
|
||||
((requestPerms & PERMISSIONS.MANAGE_POSTINGS) > 0 ||
|
||||
((requestPerms & PERMISSIONS.SUBMIT_POSTINGS) > 0 &&
|
||||
getUserCompanyId(cookies) === companyId))
|
||||
)
|
||||
) {
|
||||
return fail(403, { errorMessage: 'You cannot preform this action!' });
|
||||
}
|
||||
if (
|
||||
!title ||
|
||||
title === '' ||
|
||||
!address ||
|
||||
address === '' ||
|
||||
!description ||
|
||||
description === '' ||
|
||||
!employmentType ||
|
||||
employmentType === '' ||
|
||||
!address ||
|
||||
address === ''
|
||||
) {
|
||||
return fail(400, { errorMessage: 'All fields are required' });
|
||||
}
|
||||
|
||||
let id: number;
|
||||
try {
|
||||
id = await createPosting(<Posting>{
|
||||
title,
|
||||
description,
|
||||
employerId,
|
||||
address,
|
||||
employmentType,
|
||||
wage,
|
||||
link,
|
||||
tagIds: tagIds?.split(',').map((tag) => parseInt(tag)),
|
||||
companyId,
|
||||
flyerLink
|
||||
});
|
||||
} catch (err) {
|
||||
return fail(500, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
redirect(301, `/postings/${id}`);
|
||||
}
|
||||
};
|
||||
112
src/routes/postings/create/+page.svelte
Normal file
112
src/routes/postings/create/+page.svelte
Normal file
@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import { userState } from '$lib/shared.svelte';
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="base-container">
|
||||
<div class="content">
|
||||
<div class="elevated separator-borders m-4 rounded">
|
||||
<div class="bottom-border flex place-content-between">
|
||||
<div class="p-3 font-semibold">Create new posting</div>
|
||||
</div>
|
||||
<form method="POST" class="px-4" autocomplete="off" use:enhance>
|
||||
{#if !userState.companyId}
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Company ID <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="number"
|
||||
name="companyId"
|
||||
id="companyId"
|
||||
placeholder="Company ID"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Title <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder="Title"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Description <span class="text-red-500">*</span>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
rows="4"
|
||||
placeholder="Description"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Address <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
id="address"
|
||||
placeholder="Address"
|
||||
class="w-full rounded font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
<label for="employmentType">Employment type <span class="text-red-500">*</span></label>
|
||||
<select name="employmentType" id="employmentType" class="w-full rounded font-normal">
|
||||
<option value="full_time">Full time</option>
|
||||
<option value="part_time">Part time</option>
|
||||
<option value="internship">Internship</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Wage (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="wage"
|
||||
id="wage"
|
||||
placeholder="Wage"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Link to external posting information (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="link"
|
||||
id="link"
|
||||
placeholder="Link"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Link to flyer (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="flyerLink"
|
||||
id="flyerLink"
|
||||
placeholder="Flyer link"
|
||||
class="w-full rounded font-normal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#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"
|
||||
type="submit"
|
||||
formaction="?/submit">Create posting</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -3,24 +3,11 @@ import { type Actions, type Cookies, fail, redirect } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { createUser } from '$lib/db/index.server';
|
||||
|
||||
import { setJWT } from '$lib/shared.server';
|
||||
import type { User } from '$lib/types';
|
||||
|
||||
dotenv.config({ path: '.env' });
|
||||
|
||||
function setJWT(cookies: Cookies, username: string, perms: number) {
|
||||
const payload = {
|
||||
username: username,
|
||||
perms: perms
|
||||
};
|
||||
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw new Error('JWT_SECRET not defined');
|
||||
}
|
||||
|
||||
const maxAge = 60 * 60 * 24 * 30; // 30 days
|
||||
const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' });
|
||||
cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false });
|
||||
console.log(cookies.get('jwt'));
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
register: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
@ -28,9 +15,11 @@ export const actions: Actions = {
|
||||
const password = data.get('password')?.toString().trim();
|
||||
const confirmPassword = data.get('confirmPassword')?.toString().trim();
|
||||
let email: string | undefined | null = data.get('email')?.toString().trim();
|
||||
let phone: string | undefined | null = data.get('phone')?.toString().trim();
|
||||
let companyCode: string | undefined | null = data.get('companyCode')?.toString().trim();
|
||||
let fullName: string | undefined | null = data.get('fullName')?.toString().trim();
|
||||
if (email === '') email = null;
|
||||
if (fullName === '') fullName = null;
|
||||
if (phone === '') phone = null;
|
||||
if (companyCode === '') companyCode = null;
|
||||
|
||||
if (email && !email.includes('@')) {
|
||||
return fail(400, { errorMessage: 'Invalid email' });
|
||||
@ -40,33 +29,39 @@ export const actions: Actions = {
|
||||
username &&
|
||||
password &&
|
||||
confirmPassword &&
|
||||
email &&
|
||||
fullName &&
|
||||
username !== '' &&
|
||||
password !== '' &&
|
||||
confirmPassword !== ''
|
||||
confirmPassword !== '' &&
|
||||
email !== '' &&
|
||||
fullName !== ''
|
||||
) {
|
||||
if (password.length < 8) {
|
||||
return fail(400, { errorMessage: 'Password must be at least 8 characters' });
|
||||
}
|
||||
if (password === confirmPassword) {
|
||||
try {
|
||||
await createUser(<User>{
|
||||
const user: User = <User>{
|
||||
username: username,
|
||||
password: password,
|
||||
perms: 3,
|
||||
active: true,
|
||||
email: email,
|
||||
phone: phone,
|
||||
fullName: fullName
|
||||
});
|
||||
};
|
||||
if (password === confirmPassword) {
|
||||
try {
|
||||
await createUser(user);
|
||||
} catch (err) {
|
||||
return fail(400, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
setJWT(cookies, username, 1);
|
||||
setJWT(cookies, user);
|
||||
throw redirect(303, '/');
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Passwords do not match' });
|
||||
}
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Missing username or password' });
|
||||
return fail(400, { errorMessage: 'Please fill out all required fields' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,27 +1,123 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
import { telFormatter } from '$lib/shared.svelte';
|
||||
|
||||
onMount(() => {
|
||||
if (document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/account';
|
||||
}
|
||||
document.getElementById('phone')?.addEventListener('input', function (this: HTMLInputElement) {
|
||||
this.value = telFormatter(this.value);
|
||||
});
|
||||
});
|
||||
|
||||
// receive form data from server
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="signin-container place-items-center pt-8">
|
||||
<div class="elevated separator-borders bg content rounded-md p-8">
|
||||
<h1 class=" text-weight-semibold mb-4 text-center">Register</h1>
|
||||
<h3>Are you an applicant or an employer?</h3>
|
||||
<h1 class="text-weight-semibold mb-4 text-center">Register</h1>
|
||||
<p>Create your account. Its free and only takes a minute!</p>
|
||||
<form method="POST" class="arrange-vertically" use:enhance>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Username <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Username"
|
||||
class="input-field w-full font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="relative mt-4 text-sm font-semibold">
|
||||
Password <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="password"
|
||||
class="input-field w-full font-normal"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="relative mt-4 text-sm font-semibold">
|
||||
Confirm password <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="password"
|
||||
class="input-field w-full font-normal"
|
||||
placeholder="Password"
|
||||
name="confirmPassword"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Full name <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
placeholder="Full name"
|
||||
class="input-field w-full font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="Email"
|
||||
class="input-field w-full font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Phone (optional)
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
id="phone"
|
||||
placeholder="Phone"
|
||||
class="w-full rounded font-normal"
|
||||
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative mt-4 text-sm font-semibold">
|
||||
<label for="companyCode"> Company code (optional) </label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="companyCode"
|
||||
id="companyCode"
|
||||
placeholder="Company code"
|
||||
class="input-field w-full pr-10 font-normal"
|
||||
/>
|
||||
<a
|
||||
class="primary-bg-color mb-4 mt-6 block w-full rounded px-2 py-2 text-center"
|
||||
href="/register/user">Applicant</a
|
||||
type="button"
|
||||
href="/info#company-codes"
|
||||
class="hyperlink-color tooltip absolute inset-y-0 right-2 flex items-center"
|
||||
>
|
||||
<div class="low-emphasis-text text-center text-3xl">OR</div>
|
||||
<a
|
||||
href="/register/employer"
|
||||
class="primary-bg-color mb-2 mt-4 block w-full rounded px-2 py-2 text-center">Employer</a
|
||||
<span class="material-symbols-outlined">info</span><span
|
||||
class="tooltip-text font-sans text-sm font-normal">About company codes</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/signin" class="low-emphasis-text-button">I already have an account.</a>
|
||||
{#if form?.errorMessage}
|
||||
<div class="my-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="primary-bg-color mt-8 w-full rounded px-2 py-2"
|
||||
type="submit"
|
||||
formaction="?/register">Create account</button
|
||||
>
|
||||
<a href="/signin" class="low-emphasis-text-button mt-2">I already have an account.</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import * as dotenv from 'dotenv';
|
||||
import { type Actions, type Cookies, fail, redirect } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { createUser } from '$lib/db/index.server';
|
||||
|
||||
dotenv.config({ path: '.env' });
|
||||
|
||||
function setJWT(cookies: Cookies, username: string, perms: number) {
|
||||
const payload = {
|
||||
username: username,
|
||||
perms: perms
|
||||
};
|
||||
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw new Error('JWT_SECRET not defined');
|
||||
}
|
||||
|
||||
const maxAge = 60 * 60 * 24 * 30; // 30 days
|
||||
const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' });
|
||||
cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false });
|
||||
console.log(cookies.get('jwt'));
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
register: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
const username = data.get('username')?.toString().trim();
|
||||
const password = data.get('password')?.toString().trim();
|
||||
const confirmPassword = data.get('confirmPassword')?.toString().trim();
|
||||
|
||||
if (
|
||||
username &&
|
||||
password &&
|
||||
confirmPassword &&
|
||||
username !== '' &&
|
||||
password !== '' &&
|
||||
confirmPassword !== ''
|
||||
) {
|
||||
if (password.length < 8) {
|
||||
return fail(400, { errorMessage: 'Password must be at least 8 characters' });
|
||||
}
|
||||
if (password === confirmPassword) {
|
||||
try {
|
||||
await createUser(<User>{ username: username, password: password, perms: 3 });
|
||||
} catch (err) {
|
||||
return fail(400, { errorMessage: `Internal Server Error: ${err}` });
|
||||
}
|
||||
setJWT(cookies, username, 1);
|
||||
throw redirect(303, '/');
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Passwords do not match' });
|
||||
}
|
||||
} else {
|
||||
return fail(400, { errorMessage: 'Missing username or password' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,55 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
onMount(() => {
|
||||
if (document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/account';
|
||||
}
|
||||
});
|
||||
|
||||
// receive form data from server
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="signin-container place-items-center pt-8">
|
||||
<div class="elevated separator-borders bg content rounded-md p-8">
|
||||
<h1 class="text-weight-semibold mb-4 text-center">Register</h1>
|
||||
<p>This is for employers only!</p>
|
||||
<form method="POST" class="arrange-vertically" use:enhance>
|
||||
<input
|
||||
class="input-field my-4 w-full"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
name="username"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
class="input-field my-4 w-full"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
class="input-field mt-4 w-full"
|
||||
placeholder="Confirm password"
|
||||
name="confirmPassword"
|
||||
required
|
||||
/>
|
||||
|
||||
{#if form?.errorMessage}
|
||||
<div class="my-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="primary-bg-color mt-8 w-full rounded px-2 py-2"
|
||||
type="submit"
|
||||
formaction="?/register">Create account</button
|
||||
>
|
||||
<a href="/signin" class="low-emphasis-text-button mt-2">I already have an account.</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,85 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
onMount(() => {
|
||||
if (document.cookie.includes('jwt=')) {
|
||||
window.location.href = '/account';
|
||||
}
|
||||
});
|
||||
|
||||
// receive form data from server
|
||||
let { data, form }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="signin-container place-items-center pt-8">
|
||||
<div class="elevated separator-borders bg content rounded-md p-8">
|
||||
<h1 class="text-weight-semibold mb-4 text-center">Register</h1>
|
||||
<p>Create your account. Its free and only takes a minute!</p>
|
||||
<form method="POST" class="arrange-vertically" use:enhance>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Username <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Username"
|
||||
class="input-field w-full font-normal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Email (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="Email"
|
||||
class="input-field w-full font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4 text-sm font-semibold">
|
||||
Full name (optional)
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
id="fullName"
|
||||
placeholder="Full name"
|
||||
class="input-field w-full font-normal"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative mt-4 text-sm font-semibold">
|
||||
Password <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="password"
|
||||
class="input-field w-full font-normal"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="relative mt-4 text-sm font-semibold">
|
||||
Confirm password <span class="text-red-500">*</span>
|
||||
<input
|
||||
type="password"
|
||||
class="input-field w-full font-normal"
|
||||
placeholder="Password"
|
||||
name="confirmPassword"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if form?.errorMessage}
|
||||
<div class="my-2 text-red-500">{form.errorMessage}</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="primary-bg-color mt-8 w-full rounded px-2 py-2"
|
||||
type="submit"
|
||||
formaction="?/register">Create account</button
|
||||
>
|
||||
<a href="/signin" class="low-emphasis-text-button mt-2">I already have an account.</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,26 +1,12 @@
|
||||
import { checkUserCreds, createUser, updateLastSignin } from '$lib/db/index.server';
|
||||
import { fail, redirect, type Actions, type Cookies } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { checkUserCreds, updateLastSignin } from '$lib/db/index.server';
|
||||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
import { setJWT } from '$lib/shared.server';
|
||||
import type { User } from '$lib/types';
|
||||
|
||||
dotenv.config({ path: '.env' });
|
||||
|
||||
function setJWT(cookies: Cookies, user: User) {
|
||||
const payload = {
|
||||
username: user.username,
|
||||
perms: user.perms,
|
||||
id: user.id
|
||||
};
|
||||
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw new Error('JWT_SECRET not defined');
|
||||
}
|
||||
|
||||
const maxAge = 60 * 60 * 24 * 30; // 30 days
|
||||
const JWT = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' });
|
||||
cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false });
|
||||
}
|
||||
|
||||
export const actions: Actions = {
|
||||
signin: async ({ request, cookies }) => {
|
||||
const data = await request.formData();
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import { userState } from '$lib/shared.svelte';
|
||||
|
||||
export const getCookieValue = (name: String) =>
|
||||
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
|
||||
|
||||
export function updateUserState() {
|
||||
const JWT = getCookieValue('jwt');
|
||||
if (JWT !== '') {
|
||||
const state = JSON.parse(atob(JWT.split('.')[1]));
|
||||
userState.perms = state.perms;
|
||||
userState.username = state.username;
|
||||
userState.id = state.id;
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { sveltePreprocess } from 'svelte-preprocess';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
preprocess: sveltePreprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user