From a9ed15120144f97cd27769e36fb2560aabc3afc6 Mon Sep 17 00:00:00 2001 From: MangoSwirl Date: Sun, 14 Jul 2024 15:02:46 -0700 Subject: [PATCH 01/10] Add AttendanceLogEntry to db schema --- .../migration.sql | 11 ++++++++ prisma/schema.prisma | 26 ++++++++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 prisma/migrations/20240714220204_add_attendance_log_entries/migration.sql diff --git a/prisma/migrations/20240714220204_add_attendance_log_entries/migration.sql b/prisma/migrations/20240714220204_add_attendance_log_entries/migration.sql new file mode 100644 index 0000000..d0a2472 --- /dev/null +++ b/prisma/migrations/20240714220204_add_attendance_log_entries/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "AttendanceLogEntry" ( + "id" TEXT NOT NULL, + "personId" TEXT NOT NULL, + "timestamp" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "AttendanceLogEntry_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "AttendanceLogEntry" ADD CONSTRAINT "AttendanceLogEntry_personId_fkey" FOREIGN KEY ("personId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bc2983c..55890f0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,15 +47,16 @@ model User { } model Person { - id String @id @default(cuid()) - name String - email String - role Role @default(STUDENT) - permissions Permission[] - teamAffiliated Boolean @default(false) - labCertification LabCertification? - quizSubmissions QuizSubmission[] - profileImageUrl String? + id String @id @default(cuid()) + name String + email String + role Role @default(STUDENT) + permissions Permission[] + teamAffiliated Boolean @default(false) + labCertification LabCertification? + quizSubmissions QuizSubmission[] + attendanceLogEntries AttendanceLogEntry[] + profileImageUrl String? @@unique([email]) } @@ -91,6 +92,13 @@ enum QuizType { LAB_LAYOUT_EMERGENCY_PREPAREDNESS } +model AttendanceLogEntry { + id String @id @default(cuid()) + personId String + person Person @relation(fields: [personId], references: [id], onDelete: Cascade) + timestamp DateTime +} + model Permission { id String @id @default(cuid()) path String From 60b4ad847ea5842b6a27131c4b97b289031b772a Mon Sep 17 00:00:00 2001 From: MangoSwirl Date: Sun, 14 Jul 2024 15:09:09 -0700 Subject: [PATCH 02/10] Add badges to schema --- .../20240714220903_add_badge/migration.sql | 14 ++++++++++++++ prisma/schema.prisma | 14 +++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 prisma/migrations/20240714220903_add_badge/migration.sql diff --git a/prisma/migrations/20240714220903_add_badge/migration.sql b/prisma/migrations/20240714220903_add_badge/migration.sql new file mode 100644 index 0000000..68c98df --- /dev/null +++ b/prisma/migrations/20240714220903_add_badge/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "Badge" ( + "id" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "payload" TEXT NOT NULL, + + CONSTRAINT "Badge_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Badge_payload_key" ON "Badge"("payload"); + +-- AddForeignKey +ALTER TABLE "Badge" ADD CONSTRAINT "Badge_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 55890f0..510e21a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -56,6 +56,7 @@ model Person { labCertification LabCertification? quizSubmissions QuizSubmission[] attendanceLogEntries AttendanceLogEntry[] + badges Badge[] profileImageUrl String? @@unique([email]) @@ -93,12 +94,19 @@ enum QuizType { } model AttendanceLogEntry { - id String @id @default(cuid()) - personId String - person Person @relation(fields: [personId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + personId String + person Person @relation(fields: [personId], references: [id], onDelete: Cascade) timestamp DateTime } +model Badge { + id String @id @default(cuid()) + ownerId String + owner Person @relation(fields: [ownerId], references: [id], onDelete: Cascade) + payload String @unique +} + model Permission { id String @id @default(cuid()) path String From 6b5335bb277548c6c6ae7ca08b62eac5f0d2b0af Mon Sep 17 00:00:00 2001 From: MangoSwirl Date: Sat, 10 Aug 2024 15:46:03 -0700 Subject: [PATCH 03/10] Start badges --- package-lock.json | 619 ++++++++++++++++++ package.json | 1 + src/lib/assets/fonts/Heebo-Bold.ttf | Bin 0 -> 10576 bytes src/lib/assets/fonts/Heebo-Regular.ttf | Bin 0 -> 176100 bytes src/lib/assets/images/badge-background.png | Bin 0 -> 22243 bytes src/lib/components/Badge.svelte | 90 +++ .../api/people/[id]/badge-image/+server.ts | 75 +++ vite.config.ts | 2 +- 8 files changed, 786 insertions(+), 1 deletion(-) create mode 100644 src/lib/assets/fonts/Heebo-Bold.ttf create mode 100644 src/lib/assets/fonts/Heebo-Regular.ttf create mode 100644 src/lib/assets/images/badge-background.png create mode 100644 src/lib/components/Badge.svelte create mode 100644 src/routes/api/people/[id]/badge-image/+server.ts diff --git a/package-lock.json b/package-lock.json index eaa9312..be1f742 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "pngjs": "^7.0.0", "sharp": "^0.32.6", "sqlite3": "^5.1.6", + "svelte-component-to-image": "^0.1.0", "svelte-file-dropzone": "^2.0.7", "zod": "^3.23.8" }, @@ -1067,6 +1068,208 @@ "@prisma/debug": "5.7.0" } }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -1086,6 +1289,21 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -1263,6 +1481,11 @@ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, + "node_modules/@types/yoga-layout": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz", + "integrity": "sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", @@ -1928,6 +2151,14 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2147,6 +2378,11 @@ "readable-stream": "^3.4.0" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2258,6 +2494,14 @@ "node": ">=6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2274,6 +2518,41 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "dependencies": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz", + "integrity": "sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==", + "dependencies": { + "css-select": "^4.3.0", + "css-what": "^6.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.3.1", + "domutils": "^2.8.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -2393,6 +2672,14 @@ "color-support": "bin.js" } }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2440,6 +2727,49 @@ "node": ">= 8" } }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -2452,6 +2782,17 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2584,6 +2925,57 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -2661,6 +3053,14 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3038,6 +3438,17 @@ "node": ">=12" } }, + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3393,6 +3804,11 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3644,6 +4060,24 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -3966,6 +4400,24 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/juice": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/juice/-/juice-8.1.0.tgz", + "integrity": "sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==", + "dependencies": { + "cheerio": "1.0.0-rc.10", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4135,6 +4587,11 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4157,6 +4614,17 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -4515,6 +4983,17 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/oauth4webapi": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.4.0.tgz", @@ -4622,6 +5101,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dependencies": { + "parse5": "^6.0.1" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -4815,6 +5307,11 @@ "node": ">=4" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/preact": { "version": "10.11.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", @@ -5243,6 +5740,36 @@ "rimraf": "bin.js" } }, + "node_modules/satori": { + "version": "0.0.44", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.0.44.tgz", + "integrity": "sha512-WKUxXC2qeyno6J3ucwwLozPL6j1HXOZiN5wIUf7iqAhlx1RUC/6ePIKHi7iPc3Cy6DYuZcJriZXxXkSdo2FQHg==", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-to-react-native": "^3.0.0", + "emoji-regex": "^10.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-layout-prebuilt": "^1.10.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/satori-html": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/satori-html/-/satori-html-0.3.2.tgz", + "integrity": "sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==", + "dependencies": { + "ultrahtml": "^1.2.0" + } + }, + "node_modules/satori/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==" + }, "node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -5390,6 +5917,14 @@ "node": ">=8" } }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "engines": { + "node": "*" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -5528,6 +6063,11 @@ "node": ">=8" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==" + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5620,6 +6160,17 @@ "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" } }, + "node_modules/svelte-component-to-image": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/svelte-component-to-image/-/svelte-component-to-image-0.1.0.tgz", + "integrity": "sha512-QLHa+AbK5Zlavq9Xno9UFCUWKYVfRMeev4RvHME6D71QYd1kwt7OkR26wywxxv0Shjmon7z8cCTVKR8blsMuMw==", + "dependencies": { + "@resvg/resvg-js": "^2.2.0", + "juice": "^8.1.0", + "satori": "^0.0.44", + "satori-html": "^0.3.2" + } + }, "node_modules/svelte-eslint-parser": { "version": "0.33.1", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz", @@ -6011,6 +6562,11 @@ "node": ">=14.17" } }, + "node_modules/ultrahtml": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz", + "integrity": "sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==" + }, "node_modules/undici": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz", @@ -6075,6 +6631,14 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "engines": { + "node": ">=10" + } + }, "node_modules/vercel": { "version": "32.3.0", "resolved": "https://registry.npmjs.org/vercel/-/vercel-32.3.0.tgz", @@ -6167,6 +6731,50 @@ } } }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, "node_modules/web-vitals": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-0.2.4.tgz", @@ -6250,6 +6858,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-layout-prebuilt": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yoga-layout-prebuilt/-/yoga-layout-prebuilt-1.10.0.tgz", + "integrity": "sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g==", + "dependencies": { + "@types/yoga-layout": "1.9.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", diff --git a/package.json b/package.json index 6478e12..37852d1 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "pngjs": "^7.0.0", "sharp": "^0.32.6", "sqlite3": "^5.1.6", + "svelte-component-to-image": "^0.1.0", "svelte-file-dropzone": "^2.0.7", "zod": "^3.23.8" } diff --git a/src/lib/assets/fonts/Heebo-Bold.ttf b/src/lib/assets/fonts/Heebo-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2d72f1a41dc5ac9a5fe03007473bf34249c2b54a GIT binary patch literal 10576 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 BksSa4 literal 0 HcmV?d00001 diff --git a/src/lib/assets/fonts/Heebo-Regular.ttf b/src/lib/assets/fonts/Heebo-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b3cf8abe23fdb7419e059c6f3360e3ea5f5bb315 GIT binary patch literal 176100 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 literal 0 HcmV?d00001 diff --git a/src/lib/assets/images/badge-background.png b/src/lib/assets/images/badge-background.png new file mode 100644 index 0000000000000000000000000000000000000000..2437adca3fab647cf38d31458c0855f9e414fdbf GIT binary patch literal 22243 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 + import badgeBackground from '$lib/assets/images/badge-background.png'; + + export let name: string; + export let position: string | undefined; + export let school: string | undefined; + export let graduationYear: number | undefined; + export let imageUrl: string | undefined; + + +
+ Badge background +
+
+ {#if imageUrl} + Photo of {name} + {/if} +
+
+
{name}
+
{position}
+ {#if school} +
+ {school} +
+ {/if} + {#if graduationYear} +
+ Class of {graduationYear} +
+ {/if} +
+
+
+ + diff --git a/src/routes/api/people/[id]/badge-image/+server.ts b/src/routes/api/people/[id]/badge-image/+server.ts new file mode 100644 index 0000000..a9a2e5a --- /dev/null +++ b/src/routes/api/people/[id]/badge-image/+server.ts @@ -0,0 +1,75 @@ +import { hasPermission } from '$lib/server/util/permission/hasPermission'; +import { getPersonFromUser } from '$lib/server/util/person/getPersonFromUser'; +import prisma from '$lib/server/util/prisma'; +import { error, type RequestHandler } from '@sveltejs/kit'; +import heeboRegularUrl from '$lib/assets/fonts/Heebo-Regular.ttf'; +import heeboBoldUrl from '$lib/assets/fonts/Heebo-Bold.ttf'; +import { image_from_component } from 'svelte-component-to-image'; +import Badge from '$lib/components/Badge.svelte'; +import { localizedRole } from '$lib/util/person/role/localized'; + +export const GET: RequestHandler = async ({ params, locals, fetch }) => { + const session = await locals.getSession(); + + if (!session || !session.user) { + throw error(401); + } + + const person = await getPersonFromUser(session.user); + + if (!person) { + throw error(403, 'Account not recognized'); + } + + if (!(await hasPermission(person, 'people.view'))) { + throw error(403, 'User is not authorized'); + } + + const badgeOwner = await prisma.person.findUniqueOrThrow({ + where: { + id: params.id + } + }); + + if (!person) { + throw error(404, 'Person not found'); + } + + const IMAGE_SCALE = 6; + + const imageResponse = await fetch(`/api/people/${params.id}/picture?size=250`); + if (!imageResponse.ok) { + throw error(500); + } + // Get a base64 encoded data URL + const imageBuffer = await imageResponse.arrayBuffer(); + const profileImageUrl = `data:image/png;base64,${Buffer.from(imageBuffer).toString('base64')}`; + + console.log(profileImageUrl); + + const image = await image_from_component(Badge, { + width: 153.36 * IMAGE_SCALE, + height: 241.2 * IMAGE_SCALE, + fonts: [ + { + name: 'Heebo', + url: heeboRegularUrl, + weight: 400 + }, + { + name: 'Heebo', + url: heeboBoldUrl, + weight: 700 + } + ], + props: { + name: badgeOwner.name, + position: localizedRole(badgeOwner.role), // TODO: Make these editable + school: 'School', + graduationYear: 2026, + imageUrl: profileImageUrl + } + }); + + return new Response(image, { headers: { 'Content-Type': 'image/png' } }); +}; diff --git a/vite.config.ts b/vite.config.ts index 2f8244d..efdcffd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,6 @@ import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], server: { - origin: 'https://ubiquitous-broccoli-p9w9j6qqx67h99q-5173.app.github.dev' + origin: 'http://localhost:5173' } }); From c8a22026f947b540b347154cd55b165b55d4abf6 Mon Sep 17 00:00:00 2001 From: MangoSwirl Date: Sat, 10 Aug 2024 15:55:14 -0700 Subject: [PATCH 04/10] Add attendance source to db schema --- .../migration.sql | 16 ++++++++ prisma/schema.prisma | 40 ++++++++++++------- 2 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 prisma/migrations/20240810225426_add_attendance_source/migration.sql diff --git a/prisma/migrations/20240810225426_add_attendance_source/migration.sql b/prisma/migrations/20240810225426_add_attendance_source/migration.sql new file mode 100644 index 0000000..97a8980 --- /dev/null +++ b/prisma/migrations/20240810225426_add_attendance_source/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - Added the required column `source` to the `AttendanceLogEntry` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "AttendanceLogEntrySource" AS ENUM ('KIOSK', 'DASHBOARD'); + +-- AlterTable +ALTER TABLE "AttendanceLogEntry" ADD COLUMN "enteredById" TEXT, +ADD COLUMN "includeTime" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "source" "AttendanceLogEntrySource" NOT NULL; + +-- AddForeignKey +ALTER TABLE "AttendanceLogEntry" ADD CONSTRAINT "AttendanceLogEntry_enteredById_fkey" FOREIGN KEY ("enteredById") REFERENCES "Person"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 510e21a..3ebc36f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,17 +47,18 @@ model User { } model Person { - id String @id @default(cuid()) - name String - email String - role Role @default(STUDENT) - permissions Permission[] - teamAffiliated Boolean @default(false) - labCertification LabCertification? - quizSubmissions QuizSubmission[] - attendanceLogEntries AttendanceLogEntry[] - badges Badge[] - profileImageUrl String? + id String @id @default(cuid()) + name String + email String + role Role @default(STUDENT) + permissions Permission[] + teamAffiliated Boolean @default(false) + labCertification LabCertification? + quizSubmissions QuizSubmission[] + attendanceLogEntries AttendanceLogEntry[] @relation(name: "PersonAttendanceLogEntries") + inputtedAttendanceLogEntries AttendanceLogEntry[] @relation(name: "PersonInputtedAttendanceLogEntries") + badges Badge[] + profileImageUrl String? @@unique([email]) } @@ -93,11 +94,20 @@ enum QuizType { LAB_LAYOUT_EMERGENCY_PREPAREDNESS } +enum AttendanceLogEntrySource { + KIOSK + DASHBOARD +} + model AttendanceLogEntry { - id String @id @default(cuid()) - personId String - person Person @relation(fields: [personId], references: [id], onDelete: Cascade) - timestamp DateTime + id String @id @default(cuid()) + personId String + person Person @relation(name: "PersonAttendanceLogEntries", fields: [personId], references: [id], onDelete: Cascade) + timestamp DateTime + includeTime Boolean @default(false) + source AttendanceLogEntrySource + enteredBy Person? @relation(name: "PersonInputtedAttendanceLogEntries", fields: [enteredById], references: [id], onDelete: Cascade) + enteredById String? } model Badge { From 0fe3826c492a51dd28deb77ce40748d90508a332 Mon Sep 17 00:00:00 2001 From: MangoSwirl Date: Sat, 10 Aug 2024 23:53:14 -0700 Subject: [PATCH 05/10] Add calendar attendance view --- package-lock.json | 18 +++ package.json | 2 + .../AttendanceThumbnail.svelte | 48 ++++++++ src/lib/components/Navbar.svelte | 6 +- src/routes/tools/+page.svelte | 14 +++ .../attendance/(calendar)/month/+page.ts | 7 ++ .../(calendar)/month/[month]/+page.server.ts | 55 +++++++++ .../(calendar)/month/[month]/+page.svelte | 109 ++++++++++++++++++ src/routes/tools/attendance/+layout.svelte | 12 ++ src/routes/tools/attendance/+page.svelte | 0 src/routes/tools/attendance/+page.ts | 5 + .../{ => lab-certification}/+layout.svelte | 0 src/routes/tools/people/+layout.svelte | 6 + 13 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 src/lib/assets/images/tool-thumbnails/AttendanceThumbnail.svelte create mode 100644 src/routes/tools/attendance/(calendar)/month/+page.ts create mode 100644 src/routes/tools/attendance/(calendar)/month/[month]/+page.server.ts create mode 100644 src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte create mode 100644 src/routes/tools/attendance/+layout.svelte create mode 100644 src/routes/tools/attendance/+page.svelte create mode 100644 src/routes/tools/attendance/+page.ts rename src/routes/tools/{ => lab-certification}/+layout.svelte (100%) create mode 100644 src/routes/tools/people/+layout.svelte diff --git a/package-lock.json b/package-lock.json index be1f742..36826d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@vercel/blob": "^0.23.3", "@vercel/speed-insights": "^1.0.3", "dotenv": "^16.3.1", + "luxon": "^3.5.0", "magnolia-ui-svelte": "github:highlanderrobotics/magnolia-ui-svelte#production", "pngjs": "^7.0.0", "sharp": "^0.32.6", @@ -28,6 +29,7 @@ "@prgm/sveltekit-progress-bar": "^1.0.1", "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/kit": "^1.20.4", + "@types/luxon": "^3.4.2", "@types/pngjs": "^6.0.3", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", @@ -1451,6 +1453,13 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.10.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", @@ -4501,6 +4510,15 @@ "node": ">=10" } }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", diff --git a/package.json b/package.json index 37852d1..5ff21b4 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@prgm/sveltekit-progress-bar": "^1.0.1", "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/kit": "^1.20.4", + "@types/luxon": "^3.4.2", "@types/pngjs": "^6.0.3", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", @@ -41,6 +42,7 @@ "@vercel/blob": "^0.23.3", "@vercel/speed-insights": "^1.0.3", "dotenv": "^16.3.1", + "luxon": "^3.5.0", "magnolia-ui-svelte": "github:highlanderrobotics/magnolia-ui-svelte#production", "pngjs": "^7.0.0", "sharp": "^0.32.6", diff --git a/src/lib/assets/images/tool-thumbnails/AttendanceThumbnail.svelte b/src/lib/assets/images/tool-thumbnails/AttendanceThumbnail.svelte new file mode 100644 index 0000000..1b3b406 --- /dev/null +++ b/src/lib/assets/images/tool-thumbnails/AttendanceThumbnail.svelte @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/components/Navbar.svelte b/src/lib/components/Navbar.svelte index 72f63a9..18b69e3 100644 --- a/src/lib/components/Navbar.svelte +++ b/src/lib/components/Navbar.svelte @@ -1,6 +1,6 @@ Tools | Tooling + +
+ + + +
+

Attendance

+

+ Explore trends and view or edit specific log entries tracking attendance at team events. +

+
+
diff --git a/src/routes/tools/attendance/+layout.svelte b/src/routes/tools/attendance/+layout.svelte new file mode 100644 index 0000000..5d6a908 --- /dev/null +++ b/src/routes/tools/attendance/+layout.svelte @@ -0,0 +1,12 @@ + + + + Attendance | Tooling + + + + Attendance + + diff --git a/src/routes/tools/attendance/+page.svelte b/src/routes/tools/attendance/+page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/tools/attendance/+page.ts b/src/routes/tools/attendance/+page.ts new file mode 100644 index 0000000..6d76699 --- /dev/null +++ b/src/routes/tools/attendance/+page.ts @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit'; + +export const load = () => { + throw redirect(307, '/tools/attendance/month'); +}; diff --git a/src/routes/tools/+layout.svelte b/src/routes/tools/lab-certification/+layout.svelte similarity index 100% rename from src/routes/tools/+layout.svelte rename to src/routes/tools/lab-certification/+layout.svelte diff --git a/src/routes/tools/people/+layout.svelte b/src/routes/tools/people/+layout.svelte new file mode 100644 index 0000000..b01a41d --- /dev/null +++ b/src/routes/tools/people/+layout.svelte @@ -0,0 +1,6 @@ + + + + From 5104638b3a3c75f960563a257bc222238ebd3368 Mon Sep 17 00:00:00 2001 From: MangoSwirl Date: Sun, 11 Aug 2024 00:01:30 -0700 Subject: [PATCH 06/10] Make month nav skinnier --- .../tools/attendance/(calendar)/month/[month]/+page.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte b/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte index 212a45a..e987b4e 100644 --- a/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte +++ b/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte @@ -57,6 +57,12 @@ justify-content: space-between; align-items: center; padding: 16px; + max-width: 230px; + margin: 0 auto; + } + + .nav a { + display: contents; } .weekdays { From df6a7fdf917607e2d9ec212f9a809216c7f9c381 Mon Sep 17 00:00:00 2001 From: MangoSwirl Date: Sun, 18 Aug 2024 11:51:41 -0700 Subject: [PATCH 07/10] Add attendance dashboard --- package-lock.json | 27 ++++ package.json | 1 + .../AttendanceThumbnail.svelte | 141 ++++++++++++---- .../util/permission/localizedPermissions.ts | 5 + src/routes/+layout.svelte | 20 ++- src/routes/api/people/filtered/+server.ts | 69 ++++++++ .../tools/attendance/(calendar)/day/+page.ts | 7 + .../(calendar)/day/[date]/+page.server.ts | 151 ++++++++++++++++++ .../(calendar)/day/[date]/+page.svelte | 131 +++++++++++++++ .../day/[date]/AddAttendanceDialog.svelte | 92 +++++++++++ .../day/[date]/AttendanceEntry.svelte | 88 ++++++++++ .../(calendar)/day/[date]/PersonCard.svelte | 66 ++++++++ .../day/[date]/dailyAttendanceEntrySchema.ts | 18 +++ .../(calendar)/month/[month]/+page.server.ts | 2 +- .../(calendar)/month/[month]/+page.svelte | 27 +++- src/routes/tools/attendance/+layout.server.ts | 15 ++ .../tools/people/[id]/ProfileCardImage.svelte | 1 + 17 files changed, 817 insertions(+), 44 deletions(-) create mode 100644 src/routes/api/people/filtered/+server.ts create mode 100644 src/routes/tools/attendance/(calendar)/day/+page.ts create mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/+page.server.ts create mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/+page.svelte create mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/AddAttendanceDialog.svelte create mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/AttendanceEntry.svelte create mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/PersonCard.svelte create mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/dailyAttendanceEntrySchema.ts create mode 100644 src/routes/tools/attendance/+layout.server.ts diff --git a/package-lock.json b/package-lock.json index 36826d0..a213ce3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@auth/sveltekit": "^0.3.7", "@napi-rs/canvas": "^0.1.44", "@prisma/client": "^5.6.0", + "@tanstack/svelte-query": "^5.51.21", "@vercel/blob": "^0.23.3", "@vercel/speed-insights": "^1.0.3", "dotenv": "^16.3.1", @@ -1392,6 +1393,32 @@ "vite": "^4.0.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.51.21", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.21.tgz", + "integrity": "sha512-POQxm42IUp6n89kKWF4IZi18v3fxQWFRolvBA6phNVmA8psdfB1MvDnGacCJdS+EOX12w/CyHM62z//rHmYmvw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/svelte-query": { + "version": "5.51.21", + "resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-5.51.21.tgz", + "integrity": "sha512-NaayXSdq6LDxcbtrdo45dliFuXz7tHfDSijDyyGQv5R7bqIaK1OJ2io+mXF75ufIj1WReR1tNb9qGBOjo8/Jqw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.51.21" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-next.0" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", diff --git a/package.json b/package.json index 5ff21b4..a9d2264 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@auth/sveltekit": "^0.3.7", "@napi-rs/canvas": "^0.1.44", "@prisma/client": "^5.6.0", + "@tanstack/svelte-query": "^5.51.21", "@vercel/blob": "^0.23.3", "@vercel/speed-insights": "^1.0.3", "dotenv": "^16.3.1", diff --git a/src/lib/assets/images/tool-thumbnails/AttendanceThumbnail.svelte b/src/lib/assets/images/tool-thumbnails/AttendanceThumbnail.svelte index 1b3b406..43572a4 100644 --- a/src/lib/assets/images/tool-thumbnails/AttendanceThumbnail.svelte +++ b/src/lib/assets/images/tool-thumbnails/AttendanceThumbnail.svelte @@ -1,37 +1,118 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/src/lib/server/util/permission/localizedPermissions.ts b/src/lib/server/util/permission/localizedPermissions.ts index 9ac145c..3290bab 100644 --- a/src/lib/server/util/permission/localizedPermissions.ts +++ b/src/lib/server/util/permission/localizedPermissions.ts @@ -18,5 +18,10 @@ export const localizedPermissions: LocalizedPermissionTree = { view: "View one's own lab certification", edit: "Edit one's own lab certification" } + }, + attendance: { + '*': 'Full access to attendance', + view: 'View attendance', + edit: 'Edit attendance' } }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1ff8975..d5904d5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,19 @@ - - + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + enabled: browser + } + } + }); + - - + + + + + diff --git a/src/routes/api/people/filtered/+server.ts b/src/routes/api/people/filtered/+server.ts new file mode 100644 index 0000000..81d3f55 --- /dev/null +++ b/src/routes/api/people/filtered/+server.ts @@ -0,0 +1,69 @@ +import type { RequestHandler } from './$types'; +import prisma from '$lib/server/util/prisma'; +import { DateTime } from 'luxon'; +import { error } from '@sveltejs/kit'; + +const getFilteredPeople = async (search: string, noAttendanceOnDay: DateTime | null) => { + const people = await prisma.person.findMany({ + select: { + id: true, + name: true + }, + where: { + AND: { + OR: [ + { + name: { + contains: search, + mode: 'insensitive' + } + }, + { + email: { + contains: search, + mode: 'insensitive' + } + } + ], + attendanceLogEntries: noAttendanceOnDay + ? { + none: { + timestamp: { + lte: noAttendanceOnDay.endOf('day').toJSDate(), + gte: noAttendanceOnDay.startOf('day').toJSDate() + } + } + } + : undefined + } + }, + take: 10 + }); + + return people; +}; + +export const GET: RequestHandler = async ({ url }) => { + const { searchParams } = new URL(url); + const search = searchParams.get('search') ?? ''; + const noAttendanceOnDayString = searchParams.get('noAttendanceOnDay'); + + let noAttendanceOnDay: DateTime | null = null; + + if (noAttendanceOnDayString) { + noAttendanceOnDay = DateTime.fromFormat(noAttendanceOnDayString, 'yyyy-MM-dd'); + if (!noAttendanceOnDay.isValid) { + throw error(400, 'Invalid date'); + } + } + + const people = await getFilteredPeople(search, noAttendanceOnDay); + + return new Response(JSON.stringify(people), { + headers: { + 'Content-Type': 'application/json' + } + }); +}; + +export type FilteredPerson = Awaited>[number]; diff --git a/src/routes/tools/attendance/(calendar)/day/+page.ts b/src/routes/tools/attendance/(calendar)/day/+page.ts new file mode 100644 index 0000000..c723f4d --- /dev/null +++ b/src/routes/tools/attendance/(calendar)/day/+page.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; +import { DateTime } from 'luxon'; + +export const load = () => { + // Redirect to today's date if no date is specified + throw redirect(307, `/tools/attendance/day/${DateTime.now().toFormat('yyyy-MM-dd')}`); +}; diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/+page.server.ts b/src/routes/tools/attendance/(calendar)/day/[date]/+page.server.ts new file mode 100644 index 0000000..e4f908a --- /dev/null +++ b/src/routes/tools/attendance/(calendar)/day/[date]/+page.server.ts @@ -0,0 +1,151 @@ +import { getPersonFromUser } from '$lib/server/util/person/getPersonFromUser.js'; +import prisma from '$lib/server/util/prisma.js'; +import { AttendanceLogEntrySource } from '@prisma/client'; +import { error } from '@sveltejs/kit'; +import { DateTime } from 'luxon'; +import { DailyAttendanceEntry } from './dailyAttendanceEntrySchema'; +import { hasPermission } from '$lib/server/util/permission/hasPermission'; + +export const load = async ({ params }) => { + const date = DateTime.fromFormat(params.date, 'yyyy-MM-dd'); + + const entries = await prisma.attendanceLogEntry.findMany({ + where: { + timestamp: { + gte: date.startOf('day').toJSDate().toISOString(), + lte: date.endOf('day').toJSDate().toISOString() + } + }, + select: { + id: true, + timestamp: true, + personId: true, + enteredBy: { + select: { + name: true + } + }, + person: { + select: { + name: true, + id: true + } + }, + source: true, + includeTime: true + }, + orderBy: { + timestamp: 'asc' + } + }); + + const formatSource = (entry: (typeof entries)[number]) => { + if (entry.source === 'DASHBOARD') { + if (entry.enteredBy !== null) { + return `Recorded by ${entry.enteredBy.name}`; + } else { + return 'Recorded manually'; + } + } else if (entry.source === 'KIOSK') { + if (entry.includeTime) { + return `Checked in at ${DateTime.fromJSDate(entry.timestamp).toFormat('h:mm a')}`; + } else { + return `Checked in at kiosk`; + } + } + + return 'Unknown source'; + }; + + const attendanceLogEntries = entries.map((entry) => ({ + id: entry.id, + timestamp: entry.timestamp.toISOString(), + source: formatSource(entry), + person: entry.person, + enteredBy: entry.enteredBy + })) satisfies DailyAttendanceEntry[]; + + return { + date: date.toFormat('yyyy-MM-dd'), + attendanceLogEntries + }; +}; + +export const actions = { + addAttendance: async (event) => { + const session = await event.locals.getSession(); + if (!session?.user) throw error(500); + + const personAdding = await getPersonFromUser(session.user); + if (!personAdding) throw error(500); + + if (!(await hasPermission(personAdding, 'attendance.edit'))) { + throw error(403, 'You do not have permission to edit attendance'); + } + + const dateString = event.params.date; + const date = DateTime.fromFormat(dateString, 'yyyy-MM-dd'); + if (!date.isValid) throw error(400, 'Invalid date'); + + const data = await event.request.formData(); + + const personId = data.get('person')?.toString(); + if (!personId) throw error(400, 'No person provided'); + + const existingEntry = await prisma.attendanceLogEntry.findFirst({ + where: { + timestamp: date.toJSDate(), + person: { + id: personId + } + } + }); + + if (existingEntry !== null) { + throw error(400, 'Entry already exists'); + } + + await prisma.attendanceLogEntry.create({ + data: { + timestamp: date.toJSDate(), + person: { + connect: { + id: personId + } + }, + source: AttendanceLogEntrySource.DASHBOARD, + enteredBy: { + connect: { + id: personAdding.id + } + } + }, + include: { + person: true, + enteredBy: true + } + }); + }, + deleteAttendance: async (event) => { + const session = await event.locals.getSession(); + if (!session?.user) throw error(500); + + const personDeleting = await getPersonFromUser(session.user); + if (!personDeleting) throw error(500); + + if (!(await hasPermission(personDeleting, 'attendance.edit'))) { + throw error(403, 'You do not have permission to edit attendance'); + } + + const data = await event.request.formData(); + + const entryId = data.get('entry')?.toString(); + if (!entryId) throw error(400, 'No entry provided'); + + await prisma.attendanceLogEntry.delete({ + where: { + id: entryId + } + }); + } +}; diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/+page.svelte b/src/routes/tools/attendance/(calendar)/day/[date]/+page.svelte new file mode 100644 index 0000000..4ccb45c --- /dev/null +++ b/src/routes/tools/attendance/(calendar)/day/[date]/+page.svelte @@ -0,0 +1,131 @@ + + + + +
+ + {#each attendanceLogEntries as entry} + + {/each} +
+ + + + diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/AddAttendanceDialog.svelte b/src/routes/tools/attendance/(calendar)/day/[date]/AddAttendanceDialog.svelte new file mode 100644 index 0000000..db642f6 --- /dev/null +++ b/src/routes/tools/attendance/(calendar)/day/[date]/AddAttendanceDialog.svelte @@ -0,0 +1,92 @@ + + + +
+

Add attendee

+

If somebody attended a meeting on this day, add them here.

+ + +
+ {#if people.length > 0} +
    + {#each people as person (person.id)} + (open = false)} /> + {/each} +
+ {:else if $peopleQuery.isLoading} +
Loading...
+ {:else if $peopleQuery.isError} +
Error loading people
+ {:else if $peopleQuery.data?.length === 0} +
No people found
+ {/if} +
+
+
+ + diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/AttendanceEntry.svelte b/src/routes/tools/attendance/(calendar)/day/[date]/AttendanceEntry.svelte new file mode 100644 index 0000000..b6506dc --- /dev/null +++ b/src/routes/tools/attendance/(calendar)/day/[date]/AttendanceEntry.svelte @@ -0,0 +1,88 @@ + + +
+ Profile picture of {entry.person.name} +
+
{entry.person.name}
+
{entry.source}
+
+ +
{ + return async ({ result, update }) => { + if (result.type === 'success') { + menuOpen = false; + } + + update(); + }; + }} + on:submit={() => (deleteLoading = true)} + > + + + + + Delete + + +
+
+ + diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/PersonCard.svelte b/src/routes/tools/attendance/(calendar)/day/[date]/PersonCard.svelte new file mode 100644 index 0000000..ee24886 --- /dev/null +++ b/src/routes/tools/attendance/(calendar)/day/[date]/PersonCard.svelte @@ -0,0 +1,66 @@ + + +
  • +
    { + return async ({ result, update }) => { + if (result.type === 'success') { + onSuccess(); + } + + update(); + }; + }} + on:submit={() => (loading = true)} + > + + Profile picture of {person.name} +
    {person.name}
    + + + +
    +
  • + + diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/dailyAttendanceEntrySchema.ts b/src/routes/tools/attendance/(calendar)/day/[date]/dailyAttendanceEntrySchema.ts new file mode 100644 index 0000000..c1f7271 --- /dev/null +++ b/src/routes/tools/attendance/(calendar)/day/[date]/dailyAttendanceEntrySchema.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +export const dailyAttendanceEntrySchema = z.object({ + id: z.string(), + timestamp: z.string(), + source: z.string(), + person: z.object({ + id: z.string(), + name: z.string() + }), + enteredBy: z + .object({ + name: z.string() + }) + .or(z.null()) +}); + +export type DailyAttendanceEntry = z.infer; diff --git a/src/routes/tools/attendance/(calendar)/month/[month]/+page.server.ts b/src/routes/tools/attendance/(calendar)/month/[month]/+page.server.ts index ab40afa..4234fe4 100644 --- a/src/routes/tools/attendance/(calendar)/month/[month]/+page.server.ts +++ b/src/routes/tools/attendance/(calendar)/month/[month]/+page.server.ts @@ -49,7 +49,7 @@ export const load = async ({ params }) => { inMonth: DateTime.fromFormat(day, 'yyyy-MM-dd') >= firstDayOfMonth && DateTime.fromFormat(day, 'yyyy-MM-dd') <= lastDayOfMonth, - entryCount: entriesByDay[day]?.length ?? 0 + attendeeCount: entriesByDay[day]?.length ?? 0 })) }; }; diff --git a/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte b/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte index e987b4e..3b3f91f 100644 --- a/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte +++ b/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte @@ -34,14 +34,19 @@ @@ -88,6 +93,12 @@ flex-direction: column; height: 100px; border-radius: 7px; + text-decoration: none; + transition: background-color 0.1s ease-in-out; + } + + .day:hover { + background-color: var(--light-gray); } .day-number { @@ -98,14 +109,14 @@ border-bottom: 1px solid var(--light-gray-hover); } - .day-entries { + .day-attendees { text-align: center; - padding: 10px; + padding: 12px; font-size: 14px; color: var(--body); } - .day.has-attendees .day-entries { + .day.has-attendees .day-attendees { color: var(--victory-purple); } diff --git a/src/routes/tools/attendance/+layout.server.ts b/src/routes/tools/attendance/+layout.server.ts new file mode 100644 index 0000000..a9d5ca5 --- /dev/null +++ b/src/routes/tools/attendance/+layout.server.ts @@ -0,0 +1,15 @@ +import { hasPermission } from '$lib/server/util/permission/hasPermission'; +import { getPersonFromUser } from '$lib/server/util/person/getPersonFromUser'; +import { error } from '@sveltejs/kit'; + +export const load = async ({ locals }) => { + const session = await locals.getSession(); + if (!session?.user) throw error(500); + + const person = await getPersonFromUser(session.user); + if (!person) throw error(500); + + if (!(await hasPermission(person, 'attendance.view'))) { + throw error(403, 'You do not have permission to view attendance'); + } +}; diff --git a/src/routes/tools/people/[id]/ProfileCardImage.svelte b/src/routes/tools/people/[id]/ProfileCardImage.svelte index 4a4e5d6..c89e6de 100644 --- a/src/routes/tools/people/[id]/ProfileCardImage.svelte +++ b/src/routes/tools/people/[id]/ProfileCardImage.svelte @@ -44,6 +44,7 @@ border-radius: 7px; margin-bottom: 14px; overflow: hidden; + position: relative; } img { From ffa64b40c6deeadfab1a27b9774a58cd8c9e86f1 Mon Sep 17 00:00:00 2001 From: MangoSwirl Date: Mon, 19 Aug 2024 20:29:43 -0700 Subject: [PATCH 08/10] Add key to each block for attendance entries --- src/routes/tools/attendance/(calendar)/day/[date]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/+page.svelte b/src/routes/tools/attendance/(calendar)/day/[date]/+page.svelte index 4ccb45c..ebdda4a 100644 --- a/src/routes/tools/attendance/(calendar)/day/[date]/+page.svelte +++ b/src/routes/tools/attendance/(calendar)/day/[date]/+page.svelte @@ -52,7 +52,7 @@ > Add attendee - {#each attendanceLogEntries as entry} + {#each attendanceLogEntries as entry (entry.id)} {/each} From 99e2dd0e2e46dde7ead362e32a0000992156ada3 Mon Sep 17 00:00:00 2001 From: MangoSwirl Date: Sun, 12 Jan 2025 17:48:28 -0800 Subject: [PATCH 09/10] Badges n such --- .../migrations/20250102065348_/migration.sql | 10 ++ prisma/schema.prisma | 5 + .../util/permission/localizedPermissions.ts | 5 + src/routes/api/people/badge/+server.ts | 55 +++++++++++ .../tools/badges/assign/+page.server.ts | 86 +++++++++++++++++ src/routes/tools/badges/assign/+page.svelte | 94 +++++++++++++++++++ .../tools/badges/assign/success/+page.svelte | 25 +++++ 7 files changed, 280 insertions(+) create mode 100644 prisma/migrations/20250102065348_/migration.sql create mode 100644 src/routes/api/people/badge/+server.ts create mode 100644 src/routes/tools/badges/assign/+page.server.ts create mode 100644 src/routes/tools/badges/assign/+page.svelte create mode 100644 src/routes/tools/badges/assign/success/+page.svelte diff --git a/prisma/migrations/20250102065348_/migration.sql b/prisma/migrations/20250102065348_/migration.sql new file mode 100644 index 0000000..e2aaa58 --- /dev/null +++ b/prisma/migrations/20250102065348_/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "KioskKey" ( + "id" TEXT NOT NULL, + "key" TEXT NOT NULL, + + CONSTRAINT "KioskKey_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "KioskKey_key_key" ON "KioskKey"("key"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3ebc36f..32a68a3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -117,6 +117,11 @@ model Badge { payload String @unique } +model KioskKey { + id String @id @default(cuid()) + key String @unique +} + model Permission { id String @id @default(cuid()) path String diff --git a/src/lib/server/util/permission/localizedPermissions.ts b/src/lib/server/util/permission/localizedPermissions.ts index 3290bab..57f143a 100644 --- a/src/lib/server/util/permission/localizedPermissions.ts +++ b/src/lib/server/util/permission/localizedPermissions.ts @@ -23,5 +23,10 @@ export const localizedPermissions: LocalizedPermissionTree = { '*': 'Full access to attendance', view: 'View attendance', edit: 'Edit attendance' + }, + badges: { + '*': 'Full access to badges', + view: 'View badges', + edit: 'Edit badges' } }; diff --git a/src/routes/api/people/badge/+server.ts b/src/routes/api/people/badge/+server.ts new file mode 100644 index 0000000..d913ad7 --- /dev/null +++ b/src/routes/api/people/badge/+server.ts @@ -0,0 +1,55 @@ +import prisma from '$lib/server/util/prisma'; +import { error, RequestHandler } from '@sveltejs/kit'; + +export const GET: RequestHandler = async ({ url, request }) => { + const { searchParams } = new URL(url); + const payload = searchParams.get('payload'); + + if (!payload) { + throw error(400, 'No payload provided'); + } + + const key = request.headers.get('Authorization')?.substring(7); + if (!key) { + throw error(401, 'No key provided'); + } + + const row = await prisma.kioskKey.findFirst({ + where: { + key: key + } + }); + + if (!row) { + throw error(401, 'Invalid key'); + } + + const person = await prisma.person.findFirst({ + where: { + badges: { + some: { + payload: payload + } + } + } + }); + + if (!person) { + throw error(404, 'Badge not found'); + } + + return new Response(JSON.stringify({ person }), { + headers: { + 'Content-Type': 'application/json' + } + }); +}; + +export const OPTIONS: RequestHandler = async () => + new Response(null, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization' + } + }); diff --git a/src/routes/tools/badges/assign/+page.server.ts b/src/routes/tools/badges/assign/+page.server.ts new file mode 100644 index 0000000..488fb77 --- /dev/null +++ b/src/routes/tools/badges/assign/+page.server.ts @@ -0,0 +1,86 @@ +import { hasPermission } from '$lib/server/util/permission/hasPermission'; +import { getPersonFromUser } from '$lib/server/util/person/getPersonFromUser'; +import { error, redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import prisma from '$lib/server/util/prisma'; + +export const load = (async (event) => { + const session = await event.locals.getSession(); + if (!session?.user) throw error(500); + + const person = await getPersonFromUser(session?.user); + if (!person) throw error(500); + + const permissions = { + view: await hasPermission(person, 'badges.view'), + edit: await hasPermission(person, 'badges.edit') + }; + + if (!permissions.view) { + throw error(403, 'You do not have permission to view badges.'); + } + + // get the payload from the query params + const payload = event.url.searchParams.get('payload'); + + if (!payload) { + throw error(400, 'No payload provided'); + } + + const output: { + permissions: { + view: boolean; + edit: boolean; + }; + payload: string; + } = { permissions, payload }; + + return output; +}) satisfies PageServerLoad; + +export const actions = { + assign: async ({ request, locals }) => { + const session = await locals.getSession(); + if (!session?.user) throw error(500); + + const person = await getPersonFromUser(session?.user); + if (!person) throw error(500); + + const canEdit = await hasPermission(person, 'badges.edit'); + + if (!canEdit) { + throw error(403, 'You do not have permission to edit badges.'); + } + + const formData = await request.formData(); + + const payload = formData.get('payload'); + console.log(payload); + + if (!payload) { + console.log('no payload'); + throw error(400, 'No payload provided'); + } + + const personId = formData.get('person'); + + if (!personId) { + throw error(400, 'No person provided'); + } + + await prisma.badge.upsert({ + where: { + payload: payload.toString() + }, + update: { + ownerId: personId.toString() + }, + create: { + payload: payload.toString(), + ownerId: personId.toString() + } + }); + + throw redirect(303, `/tools/badges/assign/success`); + } +}; diff --git a/src/routes/tools/badges/assign/+page.svelte b/src/routes/tools/badges/assign/+page.svelte new file mode 100644 index 0000000..1509ab7 --- /dev/null +++ b/src/routes/tools/badges/assign/+page.svelte @@ -0,0 +1,94 @@ + + +
    +

    Assign Badge

    +

    Assign this badge to a person.

    +
    + + + +
    + {#if people.length > 0} +
      + {#each people as person (person.id)} +
    • + + +
    • + {/each} +
    + {:else if $peopleQuery.isLoading} +
    Loading...
    + {:else if $peopleQuery.isError} +
    Error loading people
    + {:else if $peopleQuery.data?.length === 0} +
    No people found
    + {/if} +
    + + +
    + + diff --git a/src/routes/tools/badges/assign/success/+page.svelte b/src/routes/tools/badges/assign/success/+page.svelte new file mode 100644 index 0000000..b056077 --- /dev/null +++ b/src/routes/tools/badges/assign/success/+page.svelte @@ -0,0 +1,25 @@ + + + + Safety Quiz + + +
    +

    Badge assigned.

    +

    You should now be able to scan it at the kiosk.

    +
    + + From 8f6b0b7579ce5c67a91e1ce12cc6d51e700e771e Mon Sep 17 00:00:00 2001 From: MangoSwirl Date: Thu, 10 Apr 2025 11:20:28 -0700 Subject: [PATCH 10/10] Remove attendance --- .../migrations/20250410182017_/migration.sql | 17 ++ prisma/schema.prisma | 38 ++--- .../util/permission/localizedPermissions.ts | 5 - src/routes/api/people/filtered/+server.ts | 28 +--- src/routes/tools/+page.svelte | 11 -- .../tools/attendance/(calendar)/day/+page.ts | 7 - .../(calendar)/day/[date]/+page.server.ts | 151 ------------------ .../(calendar)/day/[date]/+page.svelte | 131 --------------- .../day/[date]/AddAttendanceDialog.svelte | 92 ----------- .../day/[date]/AttendanceEntry.svelte | 88 ---------- .../(calendar)/day/[date]/PersonCard.svelte | 66 -------- .../day/[date]/dailyAttendanceEntrySchema.ts | 18 --- .../attendance/(calendar)/month/+page.ts | 7 - .../(calendar)/month/[month]/+page.server.ts | 55 ------- .../(calendar)/month/[month]/+page.svelte | 126 --------------- src/routes/tools/attendance/+layout.server.ts | 15 -- src/routes/tools/attendance/+layout.svelte | 12 -- src/routes/tools/attendance/+page.svelte | 0 src/routes/tools/attendance/+page.ts | 5 - .../quizzes/safety-quiz/+page.svelte | 2 +- 20 files changed, 31 insertions(+), 843 deletions(-) create mode 100644 prisma/migrations/20250410182017_/migration.sql delete mode 100644 src/routes/tools/attendance/(calendar)/day/+page.ts delete mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/+page.server.ts delete mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/+page.svelte delete mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/AddAttendanceDialog.svelte delete mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/AttendanceEntry.svelte delete mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/PersonCard.svelte delete mode 100644 src/routes/tools/attendance/(calendar)/day/[date]/dailyAttendanceEntrySchema.ts delete mode 100644 src/routes/tools/attendance/(calendar)/month/+page.ts delete mode 100644 src/routes/tools/attendance/(calendar)/month/[month]/+page.server.ts delete mode 100644 src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte delete mode 100644 src/routes/tools/attendance/+layout.server.ts delete mode 100644 src/routes/tools/attendance/+layout.svelte delete mode 100644 src/routes/tools/attendance/+page.svelte delete mode 100644 src/routes/tools/attendance/+page.ts diff --git a/prisma/migrations/20250410182017_/migration.sql b/prisma/migrations/20250410182017_/migration.sql new file mode 100644 index 0000000..e096041 --- /dev/null +++ b/prisma/migrations/20250410182017_/migration.sql @@ -0,0 +1,17 @@ +/* + Warnings: + + - You are about to drop the `AttendanceLogEntry` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "AttendanceLogEntry" DROP CONSTRAINT "AttendanceLogEntry_enteredById_fkey"; + +-- DropForeignKey +ALTER TABLE "AttendanceLogEntry" DROP CONSTRAINT "AttendanceLogEntry_personId_fkey"; + +-- DropTable +DROP TABLE "AttendanceLogEntry"; + +-- DropEnum +DROP TYPE "AttendanceLogEntrySource"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 32a68a3..4330b40 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -47,18 +47,16 @@ model User { } model Person { - id String @id @default(cuid()) - name String - email String - role Role @default(STUDENT) - permissions Permission[] - teamAffiliated Boolean @default(false) - labCertification LabCertification? - quizSubmissions QuizSubmission[] - attendanceLogEntries AttendanceLogEntry[] @relation(name: "PersonAttendanceLogEntries") - inputtedAttendanceLogEntries AttendanceLogEntry[] @relation(name: "PersonInputtedAttendanceLogEntries") - badges Badge[] - profileImageUrl String? + id String @id @default(cuid()) + name String + email String + role Role @default(STUDENT) + permissions Permission[] + teamAffiliated Boolean @default(false) + labCertification LabCertification? + quizSubmissions QuizSubmission[] + badges Badge[] + profileImageUrl String? @@unique([email]) } @@ -94,22 +92,6 @@ enum QuizType { LAB_LAYOUT_EMERGENCY_PREPAREDNESS } -enum AttendanceLogEntrySource { - KIOSK - DASHBOARD -} - -model AttendanceLogEntry { - id String @id @default(cuid()) - personId String - person Person @relation(name: "PersonAttendanceLogEntries", fields: [personId], references: [id], onDelete: Cascade) - timestamp DateTime - includeTime Boolean @default(false) - source AttendanceLogEntrySource - enteredBy Person? @relation(name: "PersonInputtedAttendanceLogEntries", fields: [enteredById], references: [id], onDelete: Cascade) - enteredById String? -} - model Badge { id String @id @default(cuid()) ownerId String diff --git a/src/lib/server/util/permission/localizedPermissions.ts b/src/lib/server/util/permission/localizedPermissions.ts index 57f143a..7e20dcc 100644 --- a/src/lib/server/util/permission/localizedPermissions.ts +++ b/src/lib/server/util/permission/localizedPermissions.ts @@ -19,11 +19,6 @@ export const localizedPermissions: LocalizedPermissionTree = { edit: "Edit one's own lab certification" } }, - attendance: { - '*': 'Full access to attendance', - view: 'View attendance', - edit: 'Edit attendance' - }, badges: { '*': 'Full access to badges', view: 'View badges', diff --git a/src/routes/api/people/filtered/+server.ts b/src/routes/api/people/filtered/+server.ts index 81d3f55..b7a168e 100644 --- a/src/routes/api/people/filtered/+server.ts +++ b/src/routes/api/people/filtered/+server.ts @@ -1,9 +1,7 @@ import type { RequestHandler } from './$types'; import prisma from '$lib/server/util/prisma'; -import { DateTime } from 'luxon'; -import { error } from '@sveltejs/kit'; -const getFilteredPeople = async (search: string, noAttendanceOnDay: DateTime | null) => { +const getFilteredPeople = async (search: string) => { const people = await prisma.person.findMany({ select: { id: true, @@ -24,17 +22,7 @@ const getFilteredPeople = async (search: string, noAttendanceOnDay: DateTime | n mode: 'insensitive' } } - ], - attendanceLogEntries: noAttendanceOnDay - ? { - none: { - timestamp: { - lte: noAttendanceOnDay.endOf('day').toJSDate(), - gte: noAttendanceOnDay.startOf('day').toJSDate() - } - } - } - : undefined + ] } }, take: 10 @@ -46,18 +34,8 @@ const getFilteredPeople = async (search: string, noAttendanceOnDay: DateTime | n export const GET: RequestHandler = async ({ url }) => { const { searchParams } = new URL(url); const search = searchParams.get('search') ?? ''; - const noAttendanceOnDayString = searchParams.get('noAttendanceOnDay'); - let noAttendanceOnDay: DateTime | null = null; - - if (noAttendanceOnDayString) { - noAttendanceOnDay = DateTime.fromFormat(noAttendanceOnDayString, 'yyyy-MM-dd'); - if (!noAttendanceOnDay.isValid) { - throw error(400, 'Invalid date'); - } - } - - const people = await getFilteredPeople(search, noAttendanceOnDay); + const people = await getFilteredPeople(search); return new Response(JSON.stringify(people), { headers: { diff --git a/src/routes/tools/+page.svelte b/src/routes/tools/+page.svelte index ed00c69..d2700b5 100644 --- a/src/routes/tools/+page.svelte +++ b/src/routes/tools/+page.svelte @@ -1,7 +1,6 @@ @@ -30,16 +29,6 @@

    - - - -
    -

    Attendance

    -

    - Explore trends and view or edit specific log entries tracking attendance at team events. -

    -
    -
    diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/AddAttendanceDialog.svelte b/src/routes/tools/attendance/(calendar)/day/[date]/AddAttendanceDialog.svelte deleted file mode 100644 index db642f6..0000000 --- a/src/routes/tools/attendance/(calendar)/day/[date]/AddAttendanceDialog.svelte +++ /dev/null @@ -1,92 +0,0 @@ - - - -
    -

    Add attendee

    -

    If somebody attended a meeting on this day, add them here.

    - - -
    - {#if people.length > 0} -
      - {#each people as person (person.id)} - (open = false)} /> - {/each} -
    - {:else if $peopleQuery.isLoading} -
    Loading...
    - {:else if $peopleQuery.isError} -
    Error loading people
    - {:else if $peopleQuery.data?.length === 0} -
    No people found
    - {/if} -
    -
    -
    - - diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/AttendanceEntry.svelte b/src/routes/tools/attendance/(calendar)/day/[date]/AttendanceEntry.svelte deleted file mode 100644 index b6506dc..0000000 --- a/src/routes/tools/attendance/(calendar)/day/[date]/AttendanceEntry.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - -
    - Profile picture of {entry.person.name} -
    -
    {entry.person.name}
    -
    {entry.source}
    -
    - -
    { - return async ({ result, update }) => { - if (result.type === 'success') { - menuOpen = false; - } - - update(); - }; - }} - on:submit={() => (deleteLoading = true)} - > - - - - - Delete - - -
    -
    - - diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/PersonCard.svelte b/src/routes/tools/attendance/(calendar)/day/[date]/PersonCard.svelte deleted file mode 100644 index ee24886..0000000 --- a/src/routes/tools/attendance/(calendar)/day/[date]/PersonCard.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - -
  • -
    { - return async ({ result, update }) => { - if (result.type === 'success') { - onSuccess(); - } - - update(); - }; - }} - on:submit={() => (loading = true)} - > - - Profile picture of {person.name} -
    {person.name}
    - - - -
    -
  • - - diff --git a/src/routes/tools/attendance/(calendar)/day/[date]/dailyAttendanceEntrySchema.ts b/src/routes/tools/attendance/(calendar)/day/[date]/dailyAttendanceEntrySchema.ts deleted file mode 100644 index c1f7271..0000000 --- a/src/routes/tools/attendance/(calendar)/day/[date]/dailyAttendanceEntrySchema.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from 'zod'; - -export const dailyAttendanceEntrySchema = z.object({ - id: z.string(), - timestamp: z.string(), - source: z.string(), - person: z.object({ - id: z.string(), - name: z.string() - }), - enteredBy: z - .object({ - name: z.string() - }) - .or(z.null()) -}); - -export type DailyAttendanceEntry = z.infer; diff --git a/src/routes/tools/attendance/(calendar)/month/+page.ts b/src/routes/tools/attendance/(calendar)/month/+page.ts deleted file mode 100644 index a9f363f..0000000 --- a/src/routes/tools/attendance/(calendar)/month/+page.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { redirect } from '@sveltejs/kit'; -import { DateTime } from 'luxon'; - -export const load = () => { - // Redirect to the current month - throw redirect(307, `/tools/attendance/month/${DateTime.now().toFormat('yyyy-MM')}`); -}; diff --git a/src/routes/tools/attendance/(calendar)/month/[month]/+page.server.ts b/src/routes/tools/attendance/(calendar)/month/[month]/+page.server.ts deleted file mode 100644 index 4234fe4..0000000 --- a/src/routes/tools/attendance/(calendar)/month/[month]/+page.server.ts +++ /dev/null @@ -1,55 +0,0 @@ -import prisma from '$lib/server/util/prisma.js'; -import { DateTime } from 'luxon'; - -export const load = async ({ params }) => { - const month = DateTime.fromFormat(params.month, 'yyyy-MM'); - - // The month component also needs the days of the previous month if they're in the same week as the first day of the current month, same for the next month - const firstDayOfMonth = month.startOf('month'); - const lastDayOfMonth = month.endOf('month'); - - const firstDayOfView = firstDayOfMonth.startOf('week'); - const lastDayOfView = lastDayOfMonth.endOf('week'); - - const entries = await prisma.attendanceLogEntry.findMany({ - where: { - timestamp: { - gte: firstDayOfView.toJSDate().toISOString(), - lte: lastDayOfView.toJSDate().toISOString() - } - }, - select: { - timestamp: true, - personId: true - }, - orderBy: { - timestamp: 'asc' - } - }); - - // Return number of entries per day - const entriesByDay = entries.reduce((acc, entry) => { - const day = DateTime.fromJSDate(entry.timestamp).toFormat('yyyy-MM-dd'); - acc[day] ??= []; - if (!acc[day].includes(entry.personId)) { - acc[day].push(entry.personId); - } - return acc; - }, {} as Record); - - const daysInViewCount = lastDayOfView.diff(firstDayOfView, 'days').days + 1; - const days = Array.from({ length: daysInViewCount }, (_, i) => - firstDayOfView.plus({ days: i }).toFormat('yyyy-MM-dd') - ); - - return { - month: month.toFormat('yyyy-MM'), - days: days.map((day) => ({ - day, - inMonth: - DateTime.fromFormat(day, 'yyyy-MM-dd') >= firstDayOfMonth && - DateTime.fromFormat(day, 'yyyy-MM-dd') <= lastDayOfMonth, - attendeeCount: entriesByDay[day]?.length ?? 0 - })) - }; -}; diff --git a/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte b/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte deleted file mode 100644 index 3b3f91f..0000000 --- a/src/routes/tools/attendance/(calendar)/month/[month]/+page.svelte +++ /dev/null @@ -1,126 +0,0 @@ - - -
    -
    - -
    - {#each days.slice(0, 7) as day} -
    {DateTime.fromFormat(day.day, 'yyyy-MM-dd').toFormat('EEE')}
    - {/each} -
    -
    - -
    - - diff --git a/src/routes/tools/attendance/+layout.server.ts b/src/routes/tools/attendance/+layout.server.ts deleted file mode 100644 index a9d5ca5..0000000 --- a/src/routes/tools/attendance/+layout.server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { hasPermission } from '$lib/server/util/permission/hasPermission'; -import { getPersonFromUser } from '$lib/server/util/person/getPersonFromUser'; -import { error } from '@sveltejs/kit'; - -export const load = async ({ locals }) => { - const session = await locals.getSession(); - if (!session?.user) throw error(500); - - const person = await getPersonFromUser(session.user); - if (!person) throw error(500); - - if (!(await hasPermission(person, 'attendance.view'))) { - throw error(403, 'You do not have permission to view attendance'); - } -}; diff --git a/src/routes/tools/attendance/+layout.svelte b/src/routes/tools/attendance/+layout.svelte deleted file mode 100644 index 5d6a908..0000000 --- a/src/routes/tools/attendance/+layout.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - Attendance | Tooling - - - - Attendance - - diff --git a/src/routes/tools/attendance/+page.svelte b/src/routes/tools/attendance/+page.svelte deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/tools/attendance/+page.ts b/src/routes/tools/attendance/+page.ts deleted file mode 100644 index 6d76699..0000000 --- a/src/routes/tools/attendance/+page.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from '@sveltejs/kit'; - -export const load = () => { - throw redirect(307, '/tools/attendance/month'); -}; diff --git a/src/routes/tools/lab-certification/quizzes/safety-quiz/+page.svelte b/src/routes/tools/lab-certification/quizzes/safety-quiz/+page.svelte index 23c368f..31129dd 100644 --- a/src/routes/tools/lab-certification/quizzes/safety-quiz/+page.svelte +++ b/src/routes/tools/lab-certification/quizzes/safety-quiz/+page.svelte @@ -8,7 +8,7 @@ export let data: PageData; - const questions: Question[] = $data.questions; + const questions: Question[] = data.questions; let loading = false;