diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 00000000..4303aa90 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "pia-dev-60cea" + } +} diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 00000000..03ce161f --- /dev/null +++ b/.github/workflows/build-and-deploy.yml @@ -0,0 +1,50 @@ +name: Build and Deploy + +on: + workflow_call: + inputs: + environment: + required: true + type: string + firebaseProjectId: + required: true + type: string + firebaseHostingChannelId: + required: false + type: string + +jobs: + build_and_preview: + name: Build and Deploy + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Create Frontend .env + run: | + cd frontend + touch .env + echo FIREBASE_CONFIG=${{ secrets.FIREBASE_CONFIG }} >> .env + - name: Create Backend .env + run: | + cd backend + touch .env + echo MONGO_URI=${{ secrets.MONGO_URI }} >> .env + echo SERVICE_ACCOUNT_KEY=${{ secrets.SERVICE_ACCOUNT_KEY }} >> .env + echo APP_PORT=${{ secrets.APP_PORT }} >> .env + - name: Build Frontend + run: cd frontend && npm ci && npm run build + - name: Build Backend + run: | + cd backend + npm run build + - name: Deploy to Firebase + uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: "${{ secrets.GITHUB_TOKEN }}" + firebaseServiceAccount: "${{ secrets.FIREBASE_SERVICE_ACCOUNT_PIA_DEV_60CEA }}" + projectId: ${{ inputs.firebaseProjectId }} + channelId: ${{ inputs.firebaseHostingChannelId }} + env: + FIREBASE_CLI_EXPERIMENTS: pintags \ No newline at end of file diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml new file mode 100644 index 00000000..6a69384a --- /dev/null +++ b/.github/workflows/preview-deploy.yml @@ -0,0 +1,13 @@ +name: Deploy Branch Preview +on: + pull_request: + +jobs: + deploy_preview: + name: Deploy Branch Preview + if: "${{ github.event.pull_request.head.repo.full_name == github.repository }}" + uses: ./.github/workflows/build-and-deploy.yml + with: + environment: Preview + firebaseProjectId: pia-dev-60cea + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml new file mode 100644 index 00000000..d7e55f75 --- /dev/null +++ b/.github/workflows/staging-deploy.yml @@ -0,0 +1,15 @@ +name: Deploy to Staging +on: + push: + branches: + - main + +jobs: + deploy_staging: + name: Deploy to Staging + uses: ./.github/workflows/build-and-deploy.yml + with: + environment: Staging + firebaseProjectId: pia-dev-60cea + firebaseHostingChannelId: live + secrets: inherit \ No newline at end of file diff --git a/.gitignore b/.gitignore index c6bba591..e5ef1cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Firebase +.firebase diff --git a/backend/package-lock.json b/backend/package-lock.json index e41ece62..584d3e17 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,8 +14,10 @@ "express": "^4.18.2", "express-validator": "^7.0.1", "firebase-admin": "^12.0.0", + "firebase-functions": "^4.7.0", "mongodb": "^6.3.0", - "mongoose": "^8.0.3" + "mongoose": "^8.0.3", + "tsc-alias": "^1.8.8" }, "devDependencies": { "@types/cors": "^2.8.17", @@ -449,7 +451,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -462,7 +463,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -471,7 +471,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -483,32 +482,27 @@ "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "optional": true + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "optional": true + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "optional": true + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "optional": true + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -517,32 +511,27 @@ "node_modules/@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "optional": true + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "optional": true + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "optional": true + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "optional": true + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "optional": true + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@tootallnate/once": { "version": "2.0.0", @@ -604,7 +593,6 @@ "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -1243,7 +1231,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1305,7 +1292,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "engines": { "node": ">=8" } @@ -1461,7 +1447,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, "engines": { "node": ">=8" } @@ -1503,7 +1488,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -1580,7 +1564,6 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, "funding": [ { "type": "individual", @@ -1647,6 +1630,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -1812,7 +1803,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "dependencies": { "path-type": "^4.0.0" }, @@ -2416,7 +2406,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2466,7 +2455,6 @@ "version": "1.16.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -2498,7 +2486,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2561,6 +2548,38 @@ "@google-cloud/storage": "^7.7.0" } }, + "node_modules/firebase-functions": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.7.0.tgz", + "integrity": "sha512-YgWqA9otWlBUouY4I2yd0vq9SyQdQ6GJxfH7wGJclzS2pzBQHcU5HhE1Vz/xTrWcKJyw8uPN98WtSE9/APUJJg==", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "node-fetch": "^2.6.7", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^10.0.0 || ^11.0.0 || ^12.0.0" + } + }, + "node_modules/firebase-functions/node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -2645,7 +2664,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -2856,7 +2874,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -2898,7 +2915,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -3208,7 +3224,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, "engines": { "node": ">= 4" } @@ -3305,7 +3320,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -3372,7 +3386,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3390,7 +3403,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -3414,7 +3426,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -3838,8 +3849,7 @@ "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", - "optional": true + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "node_modules/lru-cache": { "version": "6.0.0", @@ -3903,7 +3913,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -3920,7 +3929,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -4203,6 +4211,18 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/mylas": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz", + "integrity": "sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4221,7 +4241,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "optional": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -4240,20 +4259,17 @@ "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "optional": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "optional": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "optional": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -4271,7 +4287,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4503,7 +4518,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -4512,7 +4526,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -4520,6 +4533,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4561,7 +4585,6 @@ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", "hasInstallScript": true, - "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -4619,11 +4642,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "engines": { + "node": ">=12" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -4679,7 +4709,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -4790,7 +4819,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -4812,7 +4840,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -5014,7 +5041,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -5282,7 +5308,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -5407,6 +5432,22 @@ } } }, + "node_modules/tsc-alias": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.8.tgz", + "integrity": "sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + } + }, "node_modules/tsconfig": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index c5ad2caf..646d287f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -2,14 +2,15 @@ "name": "backend", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "dist/app.js", "scripts": { "dev": "npx ts-node-dev src/app.ts", + "build": "npm ci && tsc && tsc-alias", "format": "npm run check-git-hooks && prettier --write .", "lint-fix": "npm run check-git-hooks && (eslint --fix --cache --report-unused-disable-directives . || true) && prettier --write .", "lint-check": "npm run check-git-hooks && eslint --cache --report-unused-disable-directives . && prettier --check .", "check-git-hooks": "cd .. && node .secret-scan/secret-scan.js -- --check-git-hooks", - "prepare": "cd .. && husky install .husky" + "prepare": "if [ -z \"$GOOGLE_FUNCTION_TARGET\" ]; then cd .. && husky install .husky; fi" }, "keywords": [], "author": "", @@ -20,8 +21,10 @@ "express": "^4.18.2", "express-validator": "^7.0.1", "firebase-admin": "^12.0.0", + "firebase-functions": "^4.7.0", "mongodb": "^6.3.0", - "mongoose": "^8.0.3" + "mongoose": "^8.0.3", + "tsc-alias": "^1.8.8" }, "devDependencies": { "@types/cors": "^2.8.17", diff --git a/backend/src/app.ts b/backend/src/app.ts index 44a06dce..6d8234e2 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,12 +1,12 @@ import { json } from "body-parser"; +import cors from "cors"; import express from "express"; +import { onRequest } from "firebase-functions/v2/https"; import mongoose from "mongoose"; -import studentRoutes from "../src/routes/student"; - import { mongoURI, port } from "./config"; import { errorHandler } from "./errors/handler"; -import { userRouter } from "./routes/user"; +import router from "./routes/api"; /** * Express server application class @@ -31,9 +31,12 @@ void mongoose // Middleware server.app.use(json()); -// Routes -server.app.use("/user", userRouter); -server.app.use("/student", studentRoutes); +// sets the "Access-Control-Allow-Origin" header on all responses to allow +server.app.use(cors()); + +// Prepend /api to all routes defined in /routes/api.ts +server.app.use("/api", router); + // Error Handler server.app.use(errorHandler); @@ -41,3 +44,6 @@ server.app.use(errorHandler); server.app.listen(port, () => { console.log(`> Listening on port ${port}`); }); + +// Register our express app as a Firebase Function +export const backend = onRequest({ region: "us-west1" }, server.app); diff --git a/backend/src/config.ts b/backend/src/config.ts index b489c927..7be6fc9b 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -5,34 +5,17 @@ import { InternalError } from "./errors"; //load the env variables from .env file dotenv.config({ path: ".env" }); -let portV = ""; -let mongoV = ""; -let serviceAccountKeyV = ""; - -if (!process.env.APP_PORT) { - throw InternalError.NO_APP_PORT; -} else { - portV = process.env.APP_PORT; -} - -if (!process.env.MONGO_URI) { - throw InternalError.NO_MONGO_URI; -} else { - mongoV = process.env.MONGO_URI; -} - -if (!process.env.SERVICE_ACCOUNT_KEY) { - throw InternalError.NO_SERVICE_ACCOUNT_KEY; -} else { - serviceAccountKeyV = process.env.SERVICE_ACCOUNT_KEY; +function throwIfUndefined(envVar: string | undefined, error: InternalError) { + if (!envVar) throw error; + return envVar; } -/** - * Have to do this workaround since lint doesn't let - * us export vars - */ -const port = portV; -const mongoURI = mongoV; -const serviceAccountKey = serviceAccountKeyV; +// Check if the required env variables are defined +const port = throwIfUndefined(process.env.APP_PORT, InternalError.NO_APP_PORT); +const mongoURI = throwIfUndefined(process.env.MONGO_URI, InternalError.NO_MONGO_URI); +const serviceAccountKey = throwIfUndefined( + process.env.SERVICE_ACCOUNT_KEY, + InternalError.NO_SERVICE_ACCOUNT_KEY, +); export { port, mongoURI, serviceAccountKey }; diff --git a/backend/src/models/User.ts b/backend/src/models/user.ts similarity index 100% rename from backend/src/models/User.ts rename to backend/src/models/user.ts diff --git a/backend/src/routes/api.ts b/backend/src/routes/api.ts new file mode 100644 index 00000000..662c7931 --- /dev/null +++ b/backend/src/routes/api.ts @@ -0,0 +1,12 @@ +import express from "express"; + +import studentRoutes from "./student"; +import { userRouter } from "./user"; + +const router = express.Router(); + +// Register routers +router.use("/user", userRouter); +router.use("/student", studentRoutes); + +export default router; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index b1b1546d..36598b2d 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -55,7 +55,7 @@ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..bb3aa956 --- /dev/null +++ b/firebase.json @@ -0,0 +1,49 @@ +{ + "functions": [ + { + "runtime": "nodejs18", + "source": "backend", + "codebase": "default", + "ignore": [ + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "**/.*", + "**/node_modules/**" + ] + } + ], + "hosting": { + "public": "frontend/out", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "/api/**", + "function": { + "functionId": "backend", + "pinTag": true + } + }, + { + "source": "**", + "destination": "/index.html" + } + ] + }, + "emulators": { + "functions": { + "port": 5001 + }, + "hosting": { + "port": 3000 + }, + "ui": { + "enabled": true + }, + "singleProjectMode": true + } +} diff --git a/frontend/next.config.js b/frontend/next.config.js index 658404ac..8d7f01b5 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,4 +1,9 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + output: "export", + images: { + unoptimized: true, + }, +}; module.exports = nextConfig; diff --git a/frontend/src/components/Checkbox.tsx b/frontend/src/components/Checkbox.tsx index b4905a32..47663286 100644 --- a/frontend/src/components/Checkbox.tsx +++ b/frontend/src/components/Checkbox.tsx @@ -1,28 +1,27 @@ "use client"; -import { UseFormRegister } from "react-hook-form"; +import { FieldValues, Path, UseFormRegister } from "react-hook-form"; import { cn } from "../lib/utils"; import OtherCheckbox from "./OtherCheckbox"; -import { FormData } from "./StudentForm/types"; -type CheckboxProps = { +type CheckboxProps = { options: string[]; className?: string; - name: keyof FormData; + name: Path; defaultValue?: string[]; defaultOtherValue?: string; - register: UseFormRegister; + register: UseFormRegister; }; -export function Checkbox({ +export function Checkbox({ options, className, name, register, defaultValue, defaultOtherValue, -}: CheckboxProps) { +}: CheckboxProps) { return (
{options.map((item, index) => { diff --git a/frontend/src/components/OtherCheckbox.tsx b/frontend/src/components/OtherCheckbox.tsx index 84533275..89cf7163 100644 --- a/frontend/src/components/OtherCheckbox.tsx +++ b/frontend/src/components/OtherCheckbox.tsx @@ -1,15 +1,17 @@ import { useEffect, useState } from "react"; -import { UseFormRegister } from "react-hook-form"; +import { FieldValues, Path, UseFormRegister } from "react-hook-form"; -import { FormData } from "./StudentForm/types"; import { Textfield } from "./Textfield"; -type OtherCheckboxProps = { - register: UseFormRegister; +type OtherCheckboxProps = { + register: UseFormRegister; defaultOtherValue?: string; }; -export default function OtherCheckbox({ register, defaultOtherValue = "" }: OtherCheckboxProps) { +export default function OtherCheckbox({ + register, + defaultOtherValue = "", +}: OtherCheckboxProps) { const [checked, setChecked] = useState(defaultOtherValue !== ""); //Revert other checkbox when clicked outside @@ -32,7 +34,7 @@ export default function OtherCheckbox({ register, defaultOtherValue = "" }: Othe } label="Other" placeholder="Type Here..." defaultValue={defaultOtherValue} diff --git a/frontend/src/components/Radio.tsx b/frontend/src/components/Radio.tsx index bb4a8f4a..c7fffc89 100644 --- a/frontend/src/components/Radio.tsx +++ b/frontend/src/components/Radio.tsx @@ -1,15 +1,20 @@ -import { FieldValues, UseFormRegister } from "react-hook-form"; +import { FieldValues, Path, UseFormRegister } from "react-hook-form"; import { cn } from "../lib/utils"; -type RadioProps = { +type RadioProps = { options: string[]; className?: string; - name: string; - register: UseFormRegister; + name: Path; + register: UseFormRegister; }; -export default function Radio({ options, register, name, className }: RadioProps) { +export default function Radio({ + options, + register, + name, + className, +}: RadioProps) { return (
{options.map((option, index) => { diff --git a/frontend/src/components/StudentForm/ContactInfo.tsx b/frontend/src/components/StudentForm/ContactInfo.tsx index e24dbe2d..30defb6d 100644 --- a/frontend/src/components/StudentForm/ContactInfo.tsx +++ b/frontend/src/components/StudentForm/ContactInfo.tsx @@ -3,17 +3,17 @@ import { UseFormRegister } from "react-hook-form"; import { cn } from "../../lib/utils"; import { Textfield } from "../Textfield"; -import { FormData, StudentFormData } from "./types"; +import { StudentData, StudentFormData } from "./types"; type ContactRole = "student" | "emergency" | "serviceCoordinator"; type PersonalInfoField = "firstName" | "lastName" | "email" | "phoneNumber"; type ContactInfoProps = { - register: UseFormRegister; + register: UseFormRegister; classname?: string; type: "add" | "edit"; - data: StudentFormData | null; + data: StudentData | null; }; type FieldProps = { @@ -25,7 +25,7 @@ type FieldProps = { }; type FinalFieldProps = { - name: keyof FormData; + name: keyof StudentFormData; label: string; placeholder: string; type?: string; diff --git a/frontend/src/components/StudentForm/StudentBackground.tsx b/frontend/src/components/StudentForm/StudentBackground.tsx index f937ba8e..59d1bcea 100644 --- a/frontend/src/components/StudentForm/StudentBackground.tsx +++ b/frontend/src/components/StudentForm/StudentBackground.tsx @@ -4,13 +4,13 @@ import { cn } from "../../lib/utils"; import { Checkbox } from "../Checkbox"; import { Textfield } from "../Textfield"; -import { FormData, StudentFormData } from "./types"; +import { StudentData, StudentFormData } from "./types"; type StudentBackgroundProps = { - register: UseFormRegister; + register: UseFormRegister; classname?: string; - setCalendarValue: UseFormSetValue; - data: StudentFormData | null; + setCalendarValue: UseFormSetValue; + data: StudentData | null; }; const dietaryList = ["Nuts", "Eggs", "Seafood", "Pollen", "Dairy", "Other"]; diff --git a/frontend/src/components/StudentForm/StudentInfo.tsx b/frontend/src/components/StudentForm/StudentInfo.tsx index 4a8dbbe2..bcec3220 100644 --- a/frontend/src/components/StudentForm/StudentInfo.tsx +++ b/frontend/src/components/StudentForm/StudentInfo.tsx @@ -4,13 +4,13 @@ import { cn } from "../../lib/utils"; import { Checkbox } from "../Checkbox"; import { Textfield } from "../Textfield"; -import { FormData, StudentFormData } from "./types"; +import { StudentData, StudentFormData } from "./types"; type StudentInfoProps = { - register: UseFormRegister; + register: UseFormRegister; classname?: string; - setCalendarValue: UseFormSetValue; - data: StudentFormData | null; + setCalendarValue: UseFormSetValue; + data: StudentData | null; }; const regularPrograms = ["Intro", "ENTR"]; diff --git a/frontend/src/components/StudentForm/types.ts b/frontend/src/components/StudentForm/types.ts index eda11fa7..ec93a05b 100644 --- a/frontend/src/components/StudentForm/types.ts +++ b/frontend/src/components/StudentForm/types.ts @@ -5,7 +5,7 @@ export type Contact = { phoneNumber: string; }; -export type StudentFormData = { +export type StudentData = { student: Contact; emergency: Contact; serviceCoordinator: Contact; @@ -20,7 +20,7 @@ export type StudentFormData = { otherString: string; }; -export type FormData = { +export type StudentFormData = { student_name: string; student_last: string; student_email: string; diff --git a/frontend/src/components/StudentFormButton.tsx b/frontend/src/components/StudentFormButton.tsx index ad671d73..bd19010b 100644 --- a/frontend/src/components/StudentFormButton.tsx +++ b/frontend/src/components/StudentFormButton.tsx @@ -7,7 +7,7 @@ import { Button } from "./Button"; import ContactInfo from "./StudentForm/ContactInfo"; import StudentBackground from "./StudentForm/StudentBackground"; import StudentInfo from "./StudentForm/StudentInfo"; -import { FormData, StudentFormData } from "./StudentForm/types"; +import { StudentData, StudentFormData } from "./StudentForm/types"; import { Dialog, DialogClose, DialogContent, DialogTrigger } from "./ui/dialog"; type BaseProps = { @@ -16,12 +16,12 @@ type BaseProps = { type EditProps = BaseProps & { type: "edit"; - data: StudentFormData | null; + data: StudentData | null; }; type AddProps = BaseProps & { type: "add"; - data?: StudentFormData | null; + data?: StudentData | null; }; type StudentFormProps = EditProps | AddProps; @@ -31,10 +31,10 @@ export default function StudentFormButton({ data = null, classname, }: StudentFormProps) { - const { register, setValue: setCalendarValue, reset, handleSubmit } = useForm(); + const { register, setValue: setCalendarValue, reset, handleSubmit } = useForm(); - const onSubmit: SubmitHandler = (formData: FormData) => { - const transformedData: StudentFormData = { + const onSubmit: SubmitHandler = (formData: StudentFormData) => { + const transformedData: StudentData = { student: { firstName: formData.student_name, lastName: formData.student_last, diff --git a/frontend/src/components/Textfield.tsx b/frontend/src/components/Textfield.tsx index cfbf2264..ac2584c6 100644 --- a/frontend/src/components/Textfield.tsx +++ b/frontend/src/components/Textfield.tsx @@ -1,17 +1,15 @@ "use client"; import { useEffect, useState } from "react"; -import { UseFormRegister, UseFormSetValue } from "react-hook-form"; +import { FieldValues, Path, PathValue, UseFormRegister, UseFormSetValue } from "react-hook-form"; import { Calendar } from "../components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger } from "../components/ui/popover"; import { cn } from "../lib/utils"; -import { FormData } from "./StudentForm/types"; - -type BaseProps = { - register: UseFormRegister; - name: keyof FormData; +type BaseProps = { + register: UseFormRegister; + name: Path; label?: string; type?: string; placeholder: string; @@ -19,29 +17,29 @@ type BaseProps = { className?: string; }; -type WithCalendarProps = BaseProps & { - calendar: true; // When calendar is true, setCalendarValue is required - setCalendarValue: UseFormSetValue; +type WithCalendarProps = BaseProps & { + calendar?: true; // When calendar is false or not provided, setCalendarValue is optional + setCalendarValue?: UseFormSetValue; }; -type WithoutCalendarProps = BaseProps & { +type WithoutCalendarProps = BaseProps & { calendar?: false; // When calendar is false or not provided, setCalendarValue is optional - setCalendarValue?: UseFormSetValue; + setCalendarValue?: UseFormSetValue; }; -type TextFieldProps = WithCalendarProps | WithoutCalendarProps; +type TextFieldProps = WithCalendarProps | WithoutCalendarProps; -export function Textfield({ +export function Textfield({ register, setCalendarValue, label, - name, + name, //Must be a key in form data type specified in useForm hook placeholder, calendar = false, className, type = "text", defaultValue = "", -}: TextFieldProps) { +}: TextFieldProps) { const [date, setDate] = useState(); useEffect(() => { @@ -51,7 +49,7 @@ export function Textfield({ month: "2-digit", day: "2-digit", }); - setCalendarValue(name, parsedDate); + setCalendarValue(name, parsedDate as PathValue>); } }, [date]); @@ -64,7 +62,7 @@ export function Textfield({ )} > )} className="focus-visible:out w-full appearance-none bg-inherit px-2 placeholder-pia_accent outline-none" id={label + placeholder} type={type} diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 910d28bc..5416e497 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,11 +1,40 @@ +import { useForm } from "react-hook-form"; + +import { Button } from "../components/Button"; +import { Checkbox } from "../components/Checkbox"; import StudentFormButton from "../components/StudentFormButton"; +import { Textfield } from "../components/Textfield"; import sampleStudentData from "../sampleStudentData.json"; +type FruitData = { + fruits: string[]; + favoriteFruit: string; +}; + export default function Home() { + const { register, handleSubmit, reset } = useForm(); + + const onSubmit = (formData: FruitData) => { + console.log(formData); + reset(); + }; + return ( -
- - +
+
+ + +
+ + {/* Example */} +
+

Example

+
+ + +
); }