diff --git a/.github/workflows/unit-testing.yml b/.github/workflows/unit-testing.yml index 3a97258c..945d144c 100644 --- a/.github/workflows/unit-testing.yml +++ b/.github/workflows/unit-testing.yml @@ -38,3 +38,5 @@ jobs: GOOGLE_BASEFOLDER: ${{ secrets.GOOGLE_BASEFOLDER }} GOOGLE_CLIENT_EMAIL: ${{ secrets.GOOGLE_CLIENT_EMAIL }} GOOGLE_PRIVATE_KEY: ${{ secrets.GOOGLE_PRIVATE_KEY }} + MAILCHIMP_API_KEY: ${{ secrets.MAILCHIMP_API_KEY }} + MAILCHIMP_AUDIENCE_ID: ${{ secrets.MAILCHIMP_AUDIENCE_ID }} diff --git a/package-lock.json b/package-lock.json index 562c8719..de56ff35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.3.2", + "@mailchimp/mailchimp_marketing": "^3.0.80", "@material-tailwind/react": "^2.0.5", "@next-auth/prisma-adapter": "^1.0.4", "@prisma/client": "^5.5.2", @@ -33,15 +34,19 @@ "react-datepicker": "^6.1.0", "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", + "react-loading-skeleton": "^3.4.0", + "react-multi-carousel": "^2.8.5", "uuidv4": "^6.2.13", "yarn": "^1.22.19", "zod": "^3.22.4" }, "devDependencies": { + "@types/mailchimp__mailchimp_marketing": "^3.0.20", "@types/node": "18.0.0", "@types/react": "18.0.14", "@types/react-datepicker": "^6.0.1", "@types/react-dom": "18.0.5", + "@types/react-image-gallery": "^1.2.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", "autoprefixer": "^10.4.7", @@ -753,6 +758,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mailchimp/mailchimp_marketing": { + "version": "3.0.80", + "resolved": "https://registry.npmjs.org/@mailchimp/mailchimp_marketing/-/mailchimp_marketing-3.0.80.tgz", + "integrity": "sha512-Cgz0xPb+1DUjmrl5whAsmqfAChBko+Wf4/PLQE4RvwfPlcq2agfHr1QFiXEhZ8e+GQwQ3hZQn9iLGXwIXwxUCg==", + "dependencies": { + "dotenv": "^8.2.0", + "superagent": "3.8.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@material-tailwind/react": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@material-tailwind/react/-/react-2.1.2.tgz", @@ -1360,6 +1377,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/mailchimp__mailchimp_marketing": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/@types/mailchimp__mailchimp_marketing/-/mailchimp__mailchimp_marketing-3.0.20.tgz", + "integrity": "sha512-fg7iKnnbfBxyVjh6WZy39sXscuhaYv9K5DRAok/ykHMJeh3la4qSv+v4i5x0IgE3fGWTRZpixhCfkkzEDUImhw==", + "dev": true + }, "node_modules/@types/node": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", @@ -1436,6 +1459,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-image-gallery": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/react-image-gallery/-/react-image-gallery-1.2.4.tgz", + "integrity": "sha512-H0xpmT5rlSH0qiTvcUDCPDLRBi3J3Xa4COqaDqGb7ffLFpQoPAxpZdNuKCuThhFI0xJmNnMubZiD6B3kCBHtcw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -2504,6 +2536,14 @@ "node": ">=14" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2540,6 +2580,16 @@ "node": ">= 0.6" } }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2773,6 +2823,14 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "engines": { + "node": ">=10" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3676,6 +3734,15 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/fraction.js": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", @@ -4944,6 +5011,14 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -4957,6 +5032,17 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5834,6 +5920,11 @@ "node": ">=16.13" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5986,6 +6077,22 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-loading-skeleton": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.4.0.tgz", + "integrity": "sha512-1oJEBc9+wn7BbkQQk7YodlYEIjgeR+GrRjD+QXkVjwZN7LGIcAFHrx4NhT7UHGBxNY1+zax3c+Fo6XQM4R7CgA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-multi-carousel": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/react-multi-carousel/-/react-multi-carousel-2.8.5.tgz", + "integrity": "sha512-C5DAvJkfzR2JK9YixZ3oyF9x6R4LW6nzTpIXrl9Oujxi4uqP9SzVVCjl+JLM3tSdqdjAx/oWZK3dTVBSR73Q+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/react-onclickoutside": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz", @@ -6008,6 +6115,30 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6372,6 +6503,19 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -6564,6 +6708,48 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/superagent": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.1.tgz", + "integrity": "sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==", + "deprecated": "Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at .", + "dependencies": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.1.1", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/superagent/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6979,8 +7165,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { "version": "9.0.1", diff --git a/package.json b/package.json index 980612c2..b5649fbe 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@hookform/error-message": "^2.0.1", "@hookform/resolvers": "^3.3.2", + "@mailchimp/mailchimp_marketing": "^3.0.80", "@material-tailwind/react": "^2.0.5", "@next-auth/prisma-adapter": "^1.0.4", "@prisma/client": "^5.5.2", @@ -35,15 +36,19 @@ "react-datepicker": "^6.1.0", "react-dom": "^18.2.0", "react-hook-form": "^7.48.2", + "react-loading-skeleton": "^3.4.0", + "react-multi-carousel": "^2.8.5", "uuidv4": "^6.2.13", "yarn": "^1.22.19", "zod": "^3.22.4" }, "devDependencies": { + "@types/mailchimp__mailchimp_marketing": "^3.0.20", "@types/node": "18.0.0", "@types/react": "18.0.14", "@types/react-datepicker": "^6.0.1", "@types/react-dom": "18.0.5", + "@types/react-image-gallery": "^1.2.4", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", "autoprefixer": "^10.4.7", diff --git a/postcss.config.cjs b/postcss.config.cjs index 12a703d9..6887c826 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,5 +1,7 @@ module.exports = { plugins: { + "postcss-import": {}, + "tailwindcss/nesting": {}, tailwindcss: {}, autoprefixer: {}, }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 35e32620..7b102c5e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -69,7 +69,7 @@ model User { Seniors Senior[] @relation(fields: [SeniorIDs], references: [id]) approved Approval @default(PENDING) ChapterID String? @db.ObjectId - Chapter Chapter? @relation(fields: [ChapterID], references: [id]) + Chapter Chapter? @relation(fields: [ChapterID], references: [id], onDelete: SetNull) userRequest UserRequest? } @@ -98,7 +98,7 @@ model Senior { folder String @default("") Files File[] ChapterID String @db.ObjectId - chapter Chapter @relation(fields: [ChapterID], references: [id]) + chapter Chapter @relation(fields: [ChapterID], references: [id], onDelete: Cascade) } model File { @@ -131,6 +131,8 @@ model ChapterRequest { motivation String availabilities String questions String + + chapter Chapter? } model Chapter { @@ -144,6 +146,11 @@ model Chapter { // Google Drive API related fields chapterFolder String @default("") permissions String[] + + chapterRequestId String @db.ObjectId @unique + chapterRequest ChapterRequest @relation(fields: [chapterRequestId], references: [id], onDelete: Cascade) + + userRequests UserRequest[] } model Resource { @@ -163,6 +170,7 @@ model UserRequest { // @deprecated - We ended up not using this anywhere approved Approval @default(PENDING) uid String @unique @db.ObjectId - chapterId String @db.ObjectId + chapterId String @db.ObjectId + chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade) user User @relation(fields: [uid], references: [id]) } diff --git a/public/icons/icon_add_photo.png b/public/icons/icon_add_photo.png deleted file mode 100644 index fa9c46b4..00000000 Binary files a/public/icons/icon_add_photo.png and /dev/null differ diff --git a/public/icons/icon_audio.png b/public/icons/icon_audio.png deleted file mode 100644 index 76996a2c..00000000 Binary files a/public/icons/icon_audio.png and /dev/null differ diff --git a/public/icons/icon_doc.png b/public/icons/icon_doc.png deleted file mode 100644 index 3869d861..00000000 Binary files a/public/icons/icon_doc.png and /dev/null differ diff --git a/public/icons/icon_pdf.png b/public/icons/icon_pdf.png deleted file mode 100644 index fc9c86ae..00000000 Binary files a/public/icons/icon_pdf.png and /dev/null differ diff --git a/public/icons/icon_plus.png b/public/icons/icon_plus.png deleted file mode 100644 index 3e5b8dd5..00000000 Binary files a/public/icons/icon_plus.png and /dev/null differ diff --git a/public/icons/icon_profile.png b/public/icons/icon_profile.png deleted file mode 100644 index 3adabb9e..00000000 Binary files a/public/icons/icon_profile.png and /dev/null differ diff --git a/src/app/api/chapter/[chapterId]/route.client.ts b/src/app/api/chapter/[chapterId]/route.client.ts new file mode 100644 index 00000000..692d2ffc --- /dev/null +++ b/src/app/api/chapter/[chapterId]/route.client.ts @@ -0,0 +1,9 @@ +import { deleteChapterResponse } from "./route.schema"; + +export const deleteChapter = async (chapterId: string) => { + const response = await fetch(`/api/chapter/${chapterId}`, { + method: "DELETE", + }); + const json = await response.json(); + return deleteChapterResponse.parse(json); +}; diff --git a/src/app/api/chapter/[chapterId]/route.schema.ts b/src/app/api/chapter/[chapterId]/route.schema.ts new file mode 100644 index 00000000..ac3b529f --- /dev/null +++ b/src/app/api/chapter/[chapterId]/route.schema.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const deleteChapterResponse = z.discriminatedUnion("code", [ + z.object({ + code: z.literal("SUCCESS"), + message: z.literal("The chapter was successfully deleted"), + }), + + z.object({ + code: z.literal("CHAPTER_NOT_FOUND"), + message: z.literal("The chapter id could not be found"), + }), +]); diff --git a/src/app/api/chapter/[chapterId]/route.ts b/src/app/api/chapter/[chapterId]/route.ts index 8053d588..7e6f5a09 100644 --- a/src/app/api/chapter/[chapterId]/route.ts +++ b/src/app/api/chapter/[chapterId]/route.ts @@ -1,25 +1,30 @@ -import { NextResponse } from "next/server"; import { withSessionAndRole } from "@server/decorator"; import { prisma } from "@server/db/client"; import { driveV3 } from "@server/service"; +import { NextResponse } from "next/server"; +import { deleteChapterResponse } from "./route.schema"; export const DELETE = withSessionAndRole(["ADMIN"], async ({ params }) => { - // TODO - // 1. Implement route.client.ts - // 2. Implement route.schema.ts - // 3. Finish deleting chapter - // 4. Add it to AdminHomePage - const chapterId = params.params.chapterId; const chapter = await prisma.chapter.findUnique({ where: { id: chapterId, }, + include: { + students: true, + seniors: true, + }, }); if (chapter == null) { // If no ID is found, chapter has been deleted by another admin. - return NextResponse.json("ok"); + return NextResponse.json( + deleteChapterResponse.parse({ + code: "CHAPTER_NOT_FOUND", + message: "Chapter not found", + }), + { status: 404 } + ); } await Promise.allSettled( @@ -31,5 +36,30 @@ export const DELETE = withSessionAndRole(["ADMIN"], async ({ params }) => { ) ); - return NextResponse.json("ok"); + await prisma.user.updateMany({ + where: { + ChapterID: chapterId, + }, + data: { + SeniorIDs: { + set: [], + }, + position: "", + role: "USER", + }, + }); + + await prisma.chapterRequest.delete({ + where: { + id: chapter.chapterRequestId, + }, + }); + + return NextResponse.json( + deleteChapterResponse.parse({ + code: "SUCCESS", + message: "The chapter was successfully deleted", + }), + { status: 200 } + ); }); diff --git a/src/app/api/emails/route.ts b/src/app/api/emails/route.ts index ee624c21..4313acf3 100644 --- a/src/app/api/emails/route.ts +++ b/src/app/api/emails/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from "next/server"; import { Email, EmailResponse } from "./route.schema"; import { unknownErrorResponse } from "../route.schema"; import { prisma } from "@server/db/client"; +import { mailchimp } from "@server/service"; +import { env } from "@env/server.mjs"; export const POST = async (request: NextRequest) => { try { @@ -56,6 +58,11 @@ export const POST = async (request: NextRequest) => { }, }); + await mailchimp.lists.addListMember(env.MAILCHIMP_AUDIENCE_ID, { + email_address: body.email, + status: "subscribed", + }); + return NextResponse.json( EmailResponse.parse({ code: "SUCCESS", diff --git a/src/app/api/handle-chapter-request/route.ts b/src/app/api/handle-chapter-request/route.ts index d6d66523..62efe493 100644 --- a/src/app/api/handle-chapter-request/route.ts +++ b/src/app/api/handle-chapter-request/route.ts @@ -59,6 +59,7 @@ export const POST = withSession(async ({ req }) => { data: { chapterName: chapterRequest.university, location: chapterRequest.universityAddress, + chapterRequestId: chapterRequest.id, }, }); diff --git a/src/app/api/senior/[id]/route.schema.ts b/src/app/api/senior/[id]/route.schema.ts index 2d570a65..5027630f 100644 --- a/src/app/api/senior/[id]/route.schema.ts +++ b/src/app/api/senior/[id]/route.schema.ts @@ -6,7 +6,7 @@ import { import { seniorSchema } from "@server/model"; export const seniorDeleteResponse = z.discriminatedUnion("code", [ - z.object({ code: z.literal("SUCCESS") }), + z.object({ code: z.literal("SUCCESS"), seniorId: z.string() }), z.object({ code: z.literal("NOT_FOUND"), message: z.string(), diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index 63e289dd..38e33072 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -6,10 +6,9 @@ import { patchSeniorSchema, } from "./route.schema"; import { prisma } from "@server/db/client"; +import { driveV3 } from "@server/service"; +import { Senior } from "@prisma/client"; -/** - * @TODO - Delete folder belonging to the senior - */ export const DELETE = withSessionAndRole( ["CHAPTER_LEADER"], async ({ session, params }) => { @@ -39,23 +38,26 @@ export const DELETE = withSessionAndRole( ); } - const disconnectSenior = await prisma.senior.update({ - where: { - id: seniorId, - }, - data: { - Students: { - set: [], + await driveV3.files.delete({ fileId: maybeSenior.folder }); + await prisma.$transaction([ + prisma.senior.update({ + where: { + id: seniorId, }, - }, - }); - const deleteSenior = await prisma.senior.delete({ - where: { - id: seniorId, - }, - }); + data: { + Students: { + set: [], + }, + }, + }), + prisma.senior.delete({ + where: { + id: seniorId, + }, + }), + ]); - return NextResponse.json({ code: "SUCCESS" }); + return NextResponse.json({ code: "SUCCESS", seniorId: seniorId }); } ); @@ -118,6 +120,18 @@ export const PATCH = withSessionAndRole( }, }); + // TODO(nickbar01234) - Refactor for to sync with POST /senior + const toFolderName = (senior: Pick) => + `${senior.firstname}_${senior.lastname}-${seniorId}`; + + if (toFolderName(seniorBody) != toFolderName(maybeSenior)) { + const params = { + fileId: maybeSenior.folder, + resource: { name: toFolderName(seniorBody) }, + }; + await driveV3.files.update(params); + } + return NextResponse.json( seniorPatchResponse.parse({ code: "SUCCESS", diff --git a/src/app/api/senior/route.ts b/src/app/api/senior/route.ts index 81f6a2d3..30cb0b71 100644 --- a/src/app/api/senior/route.ts +++ b/src/app/api/senior/route.ts @@ -74,7 +74,7 @@ export const POST = withSessionAndRole( const baseFolder = chapter.chapterFolder; // TODO: make env variable const fileMetadata = { - name: [`${seniorBody.firstname}_${seniorBody.lastname}_${senior.id}`], + name: [`${seniorBody.firstname}_${seniorBody.lastname}-${senior.id}`], mimeType: "application/vnd.google-apps.folder", parents: [baseFolder], }; diff --git a/src/app/private/admin/home/chapters/page.tsx b/src/app/private/admin/home/chapters/page.tsx index 95ad1703..99948df7 100644 --- a/src/app/private/admin/home/chapters/page.tsx +++ b/src/app/private/admin/home/chapters/page.tsx @@ -5,6 +5,7 @@ const AdminChaptersPage = async () => { const chapters = await prisma.chapter.findMany({ include: { students: true, + chapterRequest: true, }, }); diff --git a/src/app/private/admin/home/error.tsx b/src/app/private/admin/home/error.tsx new file mode 100644 index 00000000..7b33c19d --- /dev/null +++ b/src/app/private/admin/home/error.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { ErrorNavigation } from "@components/navigation"; + +const Error = () => { + return ( + + ); +}; + +export default Error; diff --git a/src/app/private/admin/home/page.tsx b/src/app/private/admin/home/page.tsx index 377d4180..f64224b4 100644 --- a/src/app/private/admin/home/page.tsx +++ b/src/app/private/admin/home/page.tsx @@ -5,6 +5,7 @@ const AdminHomePageWrapper = async () => { const chapters = await prisma.chapter.findMany({ include: { students: true, + chapterRequest: true, }, }); diff --git a/src/app/private/chapter-leader/seniors/error.tsx b/src/app/private/chapter-leader/seniors/error.tsx new file mode 100644 index 00000000..08bd3dd2 --- /dev/null +++ b/src/app/private/chapter-leader/seniors/error.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { ErrorNavigation } from "@components/navigation"; + +const Error = () => { + return ( + + ); +}; + +export default Error; diff --git a/src/app/private/chapter-leader/users/error.tsx b/src/app/private/chapter-leader/users/error.tsx new file mode 100644 index 00000000..9de45007 --- /dev/null +++ b/src/app/private/chapter-leader/users/error.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { ErrorNavigation } from "@components/navigation"; + +const Error = () => { + return ( + + ); +}; + +export default Error; diff --git a/src/app/private/error.tsx b/src/app/private/error.tsx new file mode 100644 index 00000000..df60e822 --- /dev/null +++ b/src/app/private/error.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { ErrorNavigation } from "@components/navigation"; + +const Error = () => { + return ( + + ); +}; + +export default Error; diff --git a/src/app/private/user/layout.tsx b/src/app/private/user/layout.tsx index 2fcaa795..2db88220 100644 --- a/src/app/private/user/layout.tsx +++ b/src/app/private/user/layout.tsx @@ -1,7 +1,6 @@ import { ISideBar } from "@components/Sidebar"; import { CollapsibleSidebarContainer } from "@components/container"; import { faHome, faUsers, faUser } from "@fortawesome/free-solid-svg-icons"; -import { UserContext } from "src/context/UserProvider"; interface IUserLayout { children: React.ReactNode; diff --git a/src/app/private/user/seniors/error.tsx b/src/app/private/user/seniors/error.tsx new file mode 100644 index 00000000..3d317059 --- /dev/null +++ b/src/app/private/user/seniors/error.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { ErrorNavigation } from "@components/navigation"; + +const Error = () => { + return ( + + ); +}; + +export default Error; diff --git a/src/app/public/about/page.tsx b/src/app/public/about/page.tsx index 9f644043..fded5386 100644 --- a/src/app/public/about/page.tsx +++ b/src/app/public/about/page.tsx @@ -23,11 +23,13 @@ const AboutLayout = () => { }; }, []); return ( -
- +
+ Our Story -

The Legacy Project's Origin

+

+ The Legacy Project's Origin +

The concept of{" "} @@ -154,7 +156,7 @@ const AboutLayout = () => {

)} -

+

Parallel Stories Unite

@@ -197,7 +199,9 @@ const AboutLayout = () => { )} -

Collaboration and Expansion

+

+ Collaboration and Expansion +

Arielle and Katie met in 2020, quickly connecting over their aligned passions to build intergenerational connections, provide a platform for @@ -220,7 +224,9 @@ const AboutLayout = () => { students passionate about empowering local seniors with the platform to amplify their stories.

-

TLP's National Vision

+

+ TLP's National Vision +

Having watched the Tufts chapter grow over the past two years, Arielle, Katie, and Wanda decided to found TLP as a nonprofit organization with diff --git a/src/app/public/page.tsx b/src/app/public/page.tsx index 378b9c41..4d579703 100644 --- a/src/app/public/page.tsx +++ b/src/app/public/page.tsx @@ -37,7 +37,7 @@ const HomePage = () => { -

+
About Us diff --git a/src/app/public/start-chapter/page.tsx b/src/app/public/start-chapter/page.tsx index 73102778..31fb8039 100644 --- a/src/app/public/start-chapter/page.tsx +++ b/src/app/public/start-chapter/page.tsx @@ -5,13 +5,12 @@ import NewChapterForm from "@components/NewChapterForm"; const StartChapter = () => { return (
-
-
Start a Chapter
-
- Start a chapter of the Legacy Project, Inc. on your campus today! +
+
+ Start a Chapter
-
+

This form serves to identify student leaders who are interested in starting a chapter of The Legacy Project, Inc. (TLP) at their US-based diff --git a/src/app/public/team/page.tsx b/src/app/public/team/page.tsx index 874fffa6..dab40c7c 100644 --- a/src/app/public/team/page.tsx +++ b/src/app/public/team/page.tsx @@ -28,11 +28,11 @@ const PublicLayout = () => { }; }, []); return ( -

- +
+ Meet TLP -

+

The Legacy Project, Inc. (TLP) connects college students with local elders in their community with the purpose of building strong intergenerational relationships and documenting the life histories of @@ -77,7 +77,7 @@ const PublicLayout = () => { story at a time

-
+
{ const [selectedStudents, setSelectedStudents] = useState([]); - const [currentImage, setCurrentImage] = useState( - ImageIcon - ); const [confirm, setConfirm] = useState(false); const [error, setError] = useState(false); @@ -209,7 +204,6 @@ const AddSenior = ({ const handlePopUp = () => { setShowAddSeniorPopUp(!showAddSeniorPopUp); setSelectedStudents([]); - setCurrentImage(ImageIcon); setSeniorPatch(""); // empty string used as falsey value to indicate update or patch reset(); }; @@ -220,46 +214,18 @@ const AddSenior = ({ setError(false); }; - const handleImageReplace = (event: React.ChangeEvent) => { - const files = event.target.files; - - if (!files || files.length === 0) return; - - const selectedFile = files[0]; - if (!selectedFile) return; - const reader = new FileReader(); - - reader.onload = (loadEvent: ProgressEvent) => { - const dataUrl = loadEvent.target?.result; - if (typeof dataUrl === "string") { - setCurrentImage(dataUrl); - } - }; - - reader.readAsDataURL(selectedFile); - }; return ( <> {showAddSeniorPopUp && ( - + {!confirm && !error ? ( -
-
+ +
{seniorPatch ? "Update" : "Add New"} Senior
-
-
- Description - -
-
-
-
-
+
+
+
First name
{errors?.firstname && ( -
+
{errors.firstname.message}
)}
-
+
Last name
diff --git a/src/components/AdminHomePage.tsx b/src/components/AdminHomePage.tsx index 2b93f95f..b9416068 100644 --- a/src/components/AdminHomePage.tsx +++ b/src/components/AdminHomePage.tsx @@ -2,21 +2,39 @@ import { Prisma } from "@prisma/client"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faTrashCan } from "@fortawesome/free-solid-svg-icons"; +import { faEllipsis, faTrashCan } from "@fortawesome/free-solid-svg-icons"; import { TileEdit } from "./TileGrid/TileEdit"; import { InfoTile } from "./TileGrid"; import { fullName } from "@utils"; +import { deleteChapter } from "@api/chapter/[chapterId]/route.client"; +import { useRouter } from "next/navigation"; import SearchableContainer from "./SearchableContainer"; +import ChapterRequest from "./ChapterRequest"; +import DropDownContainer from "./container/DropDownContainer"; +import { useApiThrottle } from "@hooks"; +import React from "react"; +import { Spinner } from "./skeleton"; -type ChapterWithUser = Prisma.ChapterGetPayload<{ - include: { students: true }; +type ChapterWithUserAndChapterRequest = Prisma.ChapterGetPayload<{ + include: { students: true; chapterRequest: true }; }>; type AdminHomePageProps = { - chapters: ChapterWithUser[]; + chapters: ChapterWithUserAndChapterRequest[]; }; const AdminHomePage = ({ chapters }: AdminHomePageProps) => { + const router = useRouter(); + + const [deleteChapterId, setDeleteChapterId] = React.useState(""); + const { fetching, fn: throttleDeleteChapter } = useApiThrottle({ + fn: deleteChapter, + callback: () => { + setDeleteChapterId(""); + router.refresh(); + }, + }); + return ( { options.push({ name: "Remove Chapter", - onClick: async () => { - return; + onClick: () => { + setDeleteChapterId(chapter.id); + throttleDeleteChapter(chapter.id); }, color: "#ef6767", icon: , @@ -61,7 +80,35 @@ const AdminHomePage = ({ chapters }: AdminHomePageProps) => { value: prez?.email ?? "", }, ]} - topRightButton={} + topRightButton={ + fetching && chapter.id === deleteChapterId ? ( + + ) : !fetching ? ( + + } + /> + ) : null + } + moreInformation={ + + ( +
+ {children} +
+ )} + readonly + title="" + /> +
+ } /> ); }} diff --git a/src/components/ChapterRequest.tsx b/src/components/ChapterRequest.tsx index 7634aeee..f384a36e 100644 --- a/src/components/ChapterRequest.tsx +++ b/src/components/ChapterRequest.tsx @@ -5,6 +5,7 @@ import { ChapterRequest } from "@prisma/client"; import { InfoTile } from "./TileGrid"; import { useApiThrottle } from "@hooks"; import { Spinner } from "./skeleton"; +import DropDownContainer from "./container/DropDownContainer"; interface ChapterRequestMoreInformation { question: string; @@ -13,6 +14,9 @@ interface ChapterRequestMoreInformation { interface ChapterRequestProps { chapterRequest: ChapterRequest; + readonly?: boolean; // Defaults to false + ContainerNode?: ({ children }: { children?: React.ReactNode }) => JSX.Element; + title?: string; } const MoreInformation = (props: ChapterRequestMoreInformation) => { @@ -26,7 +30,8 @@ const MoreInformation = (props: ChapterRequestMoreInformation) => { }; const ChapterRequest = (props: ChapterRequestProps) => { - const { chapterRequest: request } = props; + const { chapterRequest: request, ContainerNode, title } = props; + const readonly = props.readonly ?? false; const router = useRouter(); @@ -59,7 +64,7 @@ const ChapterRequest = (props: ChapterRequestProps) => { return ( { value: request.phoneNumber, }, ]} + ContainerNode={ContainerNode} moreInformation={ -
- {qas.map((question) => ( - - ))} - {!fetching ? ( -
-
- throttleChapterRequest({ - body: { - chapterRequestId: request.id, - approved: true, - }, - }) - } - > - Accept -
-
- throttleChapterRequest({ - body: { - chapterRequestId: request.id, - approved: false, - }, - }) - } - > - Decline -
+ readonly ? ( +
+ {qas.map((question) => ( + + ))} +
+ ) : ( + +
+ {qas.map((question) => ( + + ))} + {!fetching ? ( +
+
+ throttleChapterRequest({ + body: { + chapterRequestId: request.id, + approved: true, + }, + }) + } + > + Accept +
+
+ throttleChapterRequest({ + body: { + chapterRequestId: request.id, + approved: false, + }, + }) + } + > + Decline +
+
+ ) : ( +
+ +
+ )}
- ) : ( -
- -
- )} -
+ + ) } /> ); diff --git a/src/components/LandingFooter.tsx b/src/components/LandingFooter.tsx index 45bee39c..66dc489c 100644 --- a/src/components/LandingFooter.tsx +++ b/src/components/LandingFooter.tsx @@ -47,7 +47,7 @@ const LandingFooter = () => { }; return ( -
+

There are millions of stories waiting to be told.

It's your time to change that.

diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 5e13cb53..59b6f150 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -48,7 +48,12 @@ const Navbar = () => { return (