diff --git a/.babelrc b/.babelrc index e6973d7..1bbd4ca 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,4 @@ { - "presets": ["next/babel"], - "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]] + "presets": ["next/babel"], + "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]] } diff --git a/.eslintrc.js b/.eslintrc.js index 1566f3e..38f9829 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,23 +1,23 @@ module.exports = { - extends: ["react-app", "plugin:jsx-a11y/recommended"], - plugins: ["jsx-a11y"], - rules: { - "import/no-anonymous-default-export": "error", + extends: ["react-app", "plugin:jsx-a11y/recommended"], + plugins: ["jsx-a11y"], + rules: { + "import/no-anonymous-default-export": "error", - /** - * React does not have to be in scope with Next.js - */ - "react/react-in-jsx-scope": "off", + /** + * React does not have to be in scope with Next.js + */ + "react/react-in-jsx-scope": "off", - /** - * `next/link` puts the href on the `` tag instead of the `` tag. - */ - "jsx-a11y/anchor-is-valid": "off", + /** + * `next/link` puts the href on the `` tag instead of the `` tag. + */ + "jsx-a11y/anchor-is-valid": "off", - /** - * This rule is deprecated and does not apply to modern browsers, which - * we are targeting here. - */ - "jsx-a11y/no-onchange": "off", - }, + /** + * This rule is deprecated and does not apply to modern browsers, which + * we are targeting here. + */ + "jsx-a11y/no-onchange": "off", + }, }; diff --git a/.huskyrc.js b/.huskyrc.js index 352e03c..3526631 100644 --- a/.huskyrc.js +++ b/.huskyrc.js @@ -1,6 +1,6 @@ module.exports = { - hooks: { - // Run lint-staged before committing, config is defined in lintstagedrc. - "pre-commit": "lint-staged", - }, + hooks: { + // Run lint-staged before committing, config is defined in lintstagedrc. + "pre-commit": "lint-staged", + }, }; diff --git a/.prettierrc b/.prettierrc index 6d931cf..5b5bd99 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,13 +1,3 @@ { - "overrides": [ - { - "files": ["*.md", "*.mdx"], - "options": { - "tabWidth": 2, - "useTabs": false - } - } - ], - "proseWrap": "always", - "useTabs": true + "proseWrap": "always" } diff --git a/api/app.module.ts b/api/app.module.ts index 6ccb6d8..75acc01 100644 --- a/api/app.module.ts +++ b/api/app.module.ts @@ -4,16 +4,16 @@ import { FormsModule } from "./forms/forms.module"; import { SubmissionModule } from "./submission/submission.module"; @Module({ - imports: [ - SubmissionModule, - ConfigModule.forRoot({ - isGlobal: true, - // Next.js already loads the .env file for us. - ignoreEnvFile: true, - }), - FormsModule, - ], - controllers: [], - providers: [], + imports: [ + SubmissionModule, + ConfigModule.forRoot({ + isGlobal: true, + // Next.js already loads the .env file for us. + ignoreEnvFile: true, + }), + FormsModule, + ], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/api/firebase/firebase.service.ts b/api/firebase/firebase.service.ts index a2dbbb9..66cfe47 100644 --- a/api/firebase/firebase.service.ts +++ b/api/firebase/firebase.service.ts @@ -5,25 +5,25 @@ import admin from "firebase-admin"; @Injectable() @Dependencies(ConfigService) export class FirebaseService { - private _firestore: FirebaseFirestore.Firestore; + private _firestore: FirebaseFirestore.Firestore; - constructor(private configService: ConfigService) { - if (admin.apps.length === 0) { - admin.initializeApp({ - credential: admin.credential.cert({ - projectId: this.configService.get("NEXT_PUBLIC_FIREBASE_PROJECT_ID"), - privateKey: this.configService - .get("FIREBASE_PRIVATE_KEY") - .replace(/\\n/g, "\n"), - clientEmail: this.configService.get("FIREBASE_CLIENT_EMAIL"), - }), - }); - } + constructor(private configService: ConfigService) { + if (admin.apps.length === 0) { + admin.initializeApp({ + credential: admin.credential.cert({ + projectId: this.configService.get("NEXT_PUBLIC_FIREBASE_PROJECT_ID"), + privateKey: this.configService + .get("FIREBASE_PRIVATE_KEY") + .replace(/\\n/g, "\n"), + clientEmail: this.configService.get("FIREBASE_CLIENT_EMAIL"), + }), + }); + } - this._firestore = admin.firestore(); - } + this._firestore = admin.firestore(); + } - get firestore() { - return this._firestore; - } + get firestore() { + return this._firestore; + } } diff --git a/api/forms/forms.controller.ts b/api/forms/forms.controller.ts index 9b2d761..3434aa9 100644 --- a/api/forms/forms.controller.ts +++ b/api/forms/forms.controller.ts @@ -1,10 +1,10 @@ import { - BadRequestException, - Bind, - Controller, - Delete, - Dependencies, - Param, + BadRequestException, + Bind, + Controller, + Delete, + Dependencies, + Param, } from "@nestjs/common"; import { FirebaseService } from "api/firebase/firebase.service"; import { FormsService } from "./forms.service"; @@ -12,31 +12,31 @@ import { FormsService } from "./forms.service"; @Controller("forms") @Dependencies(FormsService, FirebaseService) export class FormsController { - constructor( - private readonly formsService: FormsService, - private readonly firebaseService: FirebaseService - ) {} + constructor( + private readonly formsService: FormsService, + private readonly firebaseService: FirebaseService + ) {} - @Delete(":id") - @Bind(Param("id")) - async deleteForm(formId: string) { - if (!formId) { - throw new BadRequestException("You have to provide a non-empty form id."); - } + @Delete(":id") + @Bind(Param("id")) + async deleteForm(formId: string) { + if (!formId) { + throw new BadRequestException("You have to provide a non-empty form id."); + } - const doc = await this.firebaseService.firestore - .collection("forms") - .doc(formId) - .get(); + const doc = await this.firebaseService.firestore + .collection("forms") + .doc(formId) + .get(); - if (!doc.exists) { - throw new BadRequestException( - `A form with the id \`${formId}\` does not exist.` - ); - } + if (!doc.exists) { + throw new BadRequestException( + `A form with the id \`${formId}\` does not exist.` + ); + } - await this.formsService.deleteFormWithSubmissions(formId); + await this.formsService.deleteFormWithSubmissions(formId); - return "Successfully deleted form."; - } + return "Successfully deleted form."; + } } diff --git a/api/forms/forms.module.ts b/api/forms/forms.module.ts index 4b374d7..1f57f71 100644 --- a/api/forms/forms.module.ts +++ b/api/forms/forms.module.ts @@ -4,7 +4,7 @@ import { FormsController } from "./forms.controller"; import { FormsService } from "./forms.service"; @Module({ - controllers: [FormsController], - providers: [FormsService, FirebaseService], + controllers: [FormsController], + providers: [FormsService, FirebaseService], }) export class FormsModule {} diff --git a/api/forms/forms.service.ts b/api/forms/forms.service.ts index 7973168..ca3366d 100644 --- a/api/forms/forms.service.ts +++ b/api/forms/forms.service.ts @@ -5,51 +5,51 @@ import { firestore } from "firebase-admin"; @Injectable() @Dependencies(FirebaseService) export class FormsService { - constructor(private readonly firebaseService: FirebaseService) {} - - async deleteFormWithSubmissions(formId: string) { - const firestore = this.firebaseService.firestore; - - const query = firestore - .collection("submissions") - .where("formId", "==", formId) - .limit(100); - - await new Promise((resolve, reject) => { - this.deleteQueryBatch(query, resolve).catch(reject); - }); - - await firestore.collection("forms").doc(formId).delete(); - } - - /** - * A recursive function to delete every document in a query with batching. - */ - private async deleteQueryBatch( - query: firestore.Query, - resolve: VoidFunction - ) { - const snapshot = await query.get(); - - const batchSize = snapshot.size; - if (batchSize === 0) { - // When there are no documents left, we are done - resolve(); - return; - } - - const firestore = this.firebaseService.firestore; - - // Delete documents in a batch - const batch = firestore.batch(); - snapshot.docs.forEach((doc) => { - batch.delete(doc.ref); - }); - await batch.commit(); - - // Recurse on the next process tick, to avoid exploding the stack. - process.nextTick(() => { - this.deleteQueryBatch(query, resolve); - }); - } + constructor(private readonly firebaseService: FirebaseService) {} + + async deleteFormWithSubmissions(formId: string) { + const firestore = this.firebaseService.firestore; + + const query = firestore + .collection("submissions") + .where("formId", "==", formId) + .limit(100); + + await new Promise((resolve, reject) => { + this.deleteQueryBatch(query, resolve).catch(reject); + }); + + await firestore.collection("forms").doc(formId).delete(); + } + + /** + * A recursive function to delete every document in a query with batching. + */ + private async deleteQueryBatch( + query: firestore.Query, + resolve: VoidFunction + ) { + const snapshot = await query.get(); + + const batchSize = snapshot.size; + if (batchSize === 0) { + // When there are no documents left, we are done + resolve(); + return; + } + + const firestore = this.firebaseService.firestore; + + // Delete documents in a batch + const batch = firestore.batch(); + snapshot.docs.forEach((doc) => { + batch.delete(doc.ref); + }); + await batch.commit(); + + // Recurse on the next process tick, to avoid exploding the stack. + process.nextTick(() => { + this.deleteQueryBatch(query, resolve); + }); + } } diff --git a/api/submission/submission.controller.ts b/api/submission/submission.controller.ts index da43f50..d75e355 100644 --- a/api/submission/submission.controller.ts +++ b/api/submission/submission.controller.ts @@ -1,11 +1,11 @@ import { - BadRequestException, - Bind, - Body, - Controller, - Dependencies, - Param, - Post, + BadRequestException, + Bind, + Body, + Controller, + Dependencies, + Param, + Post, } from "@nestjs/common"; import { JsonValue } from "type-fest"; import { SubmissionService } from "./submission.service"; @@ -13,23 +13,23 @@ import { SubmissionService } from "./submission.service"; @Controller("forms/:formId/submissions") @Dependencies(SubmissionService) export class SubmissionController { - constructor(private submissionService: SubmissionService) {} + constructor(private submissionService: SubmissionService) {} - @Post() - @Bind(Param("formId"), Body()) - async submit(formId: string | undefined, body: JsonValue) { - if (!formId) { - throw new BadRequestException("No form id specified specified."); - } + @Post() + @Bind(Param("formId"), Body()) + async submit(formId: string | undefined, body: JsonValue) { + if (!formId) { + throw new BadRequestException("No form id specified specified."); + } - if (typeof body !== "object" || body === null || Array.isArray(body)) { - throw new BadRequestException( - "The request body has to contain a JSON object." - ); - } + if (typeof body !== "object" || body === null || Array.isArray(body)) { + throw new BadRequestException( + "The request body has to contain a JSON object." + ); + } - await this.submissionService.create(formId, body); + await this.submissionService.create(formId, body); - return "Successfully submitted form."; - } + return "Successfully submitted form."; + } } diff --git a/api/submission/submission.module.ts b/api/submission/submission.module.ts index ddd363d..8d0f15f 100644 --- a/api/submission/submission.module.ts +++ b/api/submission/submission.module.ts @@ -4,8 +4,8 @@ import { SubmissionController } from "./submission.controller"; import { SubmissionService } from "./submission.service"; @Module({ - imports: [], - controllers: [SubmissionController], - providers: [SubmissionService, FirebaseService], + imports: [], + controllers: [SubmissionController], + providers: [SubmissionService, FirebaseService], }) export class SubmissionModule {} diff --git a/api/submission/submission.service.ts b/api/submission/submission.service.ts index 3e16712..0011ac6 100644 --- a/api/submission/submission.service.ts +++ b/api/submission/submission.service.ts @@ -6,29 +6,29 @@ import { JsonObject } from "type-fest"; @Injectable() @Dependencies(FirebaseService) export class SubmissionService { - constructor(private readonly firebaseService: FirebaseService) {} + constructor(private readonly firebaseService: FirebaseService) {} - async create(formId: string, data: JsonObject) { - const firestore = this.firebaseService.firestore; + async create(formId: string, data: JsonObject) { + const firestore = this.firebaseService.firestore; - const formDoc = await firestore.collection("forms").doc(formId).get(); + const formDoc = await firestore.collection("forms").doc(formId).get(); - if (!formDoc.exists) { - throw new BadRequestException( - `A form with the id \`${formId}\` does not exist.` - ); - } + if (!formDoc.exists) { + throw new BadRequestException( + `A form with the id \`${formId}\` does not exist.` + ); + } - if (!formDoc.data()?.allowSubmissions) { - throw new BadRequestException(`Submissions were disabled for this form.`); - } + if (!formDoc.data()?.allowSubmissions) { + throw new BadRequestException(`Submissions were disabled for this form.`); + } - await firestore.collection("submissions").add({ - createdAt: firebase.firestore.FieldValue.serverTimestamp(), - data, - formId, - readAt: null, - done: false, - }); - } + await firestore.collection("submissions").add({ + createdAt: firebase.firestore.FieldValue.serverTimestamp(), + data, + formId, + readAt: null, + done: false, + }); + } } diff --git a/firebase.json b/firebase.json index 34590bc..7c4c825 100644 --- a/firebase.json +++ b/firebase.json @@ -1,6 +1,6 @@ { - "firestore": { - "indexes": "firebase/firestore.indexes.json", - "rules": "firebase/firestore.rules" - } + "firestore": { + "indexes": "firebase/firestore.indexes.json", + "rules": "firebase/firestore.rules" + } } diff --git a/firebase/firestore.indexes.json b/firebase/firestore.indexes.json index c3543e2..b0cc9d8 100644 --- a/firebase/firestore.indexes.json +++ b/firebase/firestore.indexes.json @@ -1,19 +1,19 @@ { - "fieldOverrides": [], - "indexes": [ - { - "collectionGroup": "submissions", - "fields": [ - { - "fieldPath": "formId", - "order": "ASCENDING" - }, - { - "fieldPath": "createdAt", - "order": "DESCENDING" - } - ], - "queryScope": "COLLECTION" - } - ] + "fieldOverrides": [], + "indexes": [ + { + "collectionGroup": "submissions", + "fields": [ + { + "fieldPath": "formId", + "order": "ASCENDING" + }, + { + "fieldPath": "createdAt", + "order": "DESCENDING" + } + ], + "queryScope": "COLLECTION" + } + ] } diff --git a/nest-cli.json b/nest-cli.json index a1c1b9d..42e0c00 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,4 +1,4 @@ { - "collection": "@nestjs/schematics", - "sourceRoot": "api" + "collection": "@nestjs/schematics", + "sourceRoot": "api" } diff --git a/next.config.js b/next.config.js index 92a1d14..4ea76a3 100644 --- a/next.config.js +++ b/next.config.js @@ -1,29 +1,29 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({ - enabled: process.env.ANALYZE === "true", + enabled: process.env.ANALYZE === "true", }); module.exports = withBundleAnalyzer({ - webpack(config) { - config.module.rules.push({ - test: /\.svg$/, - issuer: { - test: /\.(js|ts)x?$/, - }, - use: ["@svgr/webpack"], - }); + webpack(config) { + config.module.rules.push({ + test: /\.svg$/, + issuer: { + test: /\.(js|ts)x?$/, + }, + use: ["@svgr/webpack"], + }); - return config; - }, - async rewrites() { - return [ - { - source: "/inbox", - destination: "/inbox/none", - }, - { - source: "/:first", - destination: "/:first/none", - }, - ]; - }, + return config; + }, + async rewrites() { + return [ + { + source: "/inbox", + destination: "/inbox/none", + }, + { + source: "/:first", + destination: "/:first/none", + }, + ]; + }, }); diff --git a/package.json b/package.json index 7671d5f..e07ef9b 100644 --- a/package.json +++ b/package.json @@ -1,80 +1,80 @@ { - "name": "quice", - "version": "0.1.0", - "private": true, - "scripts": { - "analyze-build": "cross-env ANALYZE=true yarn build", - "build": "next build", - "check-types": "tsc", - "dev": "next dev", - "format": "prettier . --write --ignore-path .gitignore", - "lint": "eslint . --ignore-path .gitignore", - "seed-database": "node scripts/seedDatabase.js", - "start": "next start" - }, - "dependencies": { - "@headlessui/react": "^0.2.0", - "@nestjs/common": "^7.6.11", - "@nestjs/config": "^0.6.3", - "@nestjs/core": "^7.6.11", - "@nestjs/platform-express": "^7.6.11", - "@next/bundle-analyzer": "^10.0.6", - "@svgr/webpack": "^5.5.0", - "@tailwindcss/forms": "^0.2.1", - "@tailwindcss/typography": "^0.4.0", - "autoprefixer": "^10.2.4", - "clsx": "^1.1.1", - "date-fns": "^2.17.0", - "dotenv": "^8.2.0", - "enquirer": "^2.3.6", - "faker": "^5.3.1", - "firebase": "^8.2.6", - "firebase-admin": "^9.4.2", - "framer-motion": "^3.3.0", - "heroicons": "^0.4.2", - "lodash.kebabcase": "^4.1.1", - "next": "^10.0.6", - "postcss": "^8.2.4", - "react": "^17.0.1", - "react-dom": "^17.0.1", - "react-firebase-hooks": "^2.2.0", - "react-hook-form": "^6.15.1", - "react-linkify": "^1.0.0-alpha", - "react-syntax-highlighter": "^15.4.3", - "react-virtuoso": "^1.5.4", - "reflect-metadata": "^0.1.13", - "rxjs": "^6.6.3", - "tailwindcss": "^2.0.2" - }, - "devDependencies": { - "@babel/core": "^7.12.13", - "@babel/plugin-proposal-decorators": "^7.12.13", - "@nestjs/cli": "^7.5.4", - "@nestjs/schematics": "^7.2.7", - "@types/express": "^4.17.11", - "@types/lodash.kebabcase": "^4.1.6", - "@types/node": "^14.14.25", - "@types/react": "^17.0.1", - "@types/react-dom": "^17.0.0", - "@types/react-linkify": "^1.0.0", - "@types/react-syntax-highlighter": "^13.5.0", - "@typescript-eslint/eslint-plugin": "^4.14.2", - "@typescript-eslint/parser": "^4.14.2", - "babel-eslint": "^10.1.0", - "cross-env": "^7.0.3", - "eslint": "^7.19.0", - "eslint-config-react-app": "^6.0.0", - "eslint-plugin-flowtype": "^5.2.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.22.0", - "eslint-plugin-react-hooks": "^4.2.0", - "husky": "^4.3.8", - "lint-staged": "^10.5.4", - "organize-imports-cli": "^0.8.0", - "prettier": "^2.2.1", - "prettier-plugin-packagejson": "^2.2.9", - "type-fest": "^0.20.2", - "typescript": "^4.1.3" - } + "name": "quice", + "version": "0.1.0", + "private": true, + "scripts": { + "analyze-build": "cross-env ANALYZE=true yarn build", + "build": "next build", + "check-types": "tsc", + "dev": "next dev", + "format": "prettier . --write --ignore-path .gitignore", + "lint": "eslint . --ignore-path .gitignore", + "seed-database": "node scripts/seedDatabase.js", + "start": "next start" + }, + "dependencies": { + "@headlessui/react": "^0.2.0", + "@nestjs/common": "^7.6.11", + "@nestjs/config": "^0.6.3", + "@nestjs/core": "^7.6.11", + "@nestjs/platform-express": "^7.6.11", + "@next/bundle-analyzer": "^10.0.6", + "@svgr/webpack": "^5.5.0", + "@tailwindcss/forms": "^0.2.1", + "@tailwindcss/typography": "^0.4.0", + "autoprefixer": "^10.2.4", + "clsx": "^1.1.1", + "date-fns": "^2.17.0", + "dotenv": "^8.2.0", + "enquirer": "^2.3.6", + "faker": "^5.3.1", + "firebase": "^8.2.6", + "firebase-admin": "^9.4.2", + "framer-motion": "^3.3.0", + "heroicons": "^0.4.2", + "lodash.kebabcase": "^4.1.1", + "next": "^10.0.6", + "postcss": "^8.2.4", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "react-firebase-hooks": "^2.2.0", + "react-hook-form": "^6.15.1", + "react-linkify": "^1.0.0-alpha", + "react-syntax-highlighter": "^15.4.3", + "react-virtuoso": "^1.5.4", + "reflect-metadata": "^0.1.13", + "rxjs": "^6.6.3", + "tailwindcss": "^2.0.2" + }, + "devDependencies": { + "@babel/core": "^7.12.13", + "@babel/plugin-proposal-decorators": "^7.12.13", + "@nestjs/cli": "^7.5.4", + "@nestjs/schematics": "^7.2.7", + "@types/express": "^4.17.11", + "@types/lodash.kebabcase": "^4.1.6", + "@types/node": "^14.14.25", + "@types/react": "^17.0.1", + "@types/react-dom": "^17.0.0", + "@types/react-linkify": "^1.0.0", + "@types/react-syntax-highlighter": "^13.5.0", + "@typescript-eslint/eslint-plugin": "^4.14.2", + "@typescript-eslint/parser": "^4.14.2", + "babel-eslint": "^10.1.0", + "cross-env": "^7.0.3", + "eslint": "^7.19.0", + "eslint-config-react-app": "^6.0.0", + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.22.0", + "eslint-plugin-react-hooks": "^4.2.0", + "husky": "^4.3.8", + "lint-staged": "^10.5.4", + "organize-imports-cli": "^0.8.0", + "prettier": "^2.2.1", + "prettier-plugin-packagejson": "^2.2.9", + "type-fest": "^0.20.2", + "typescript": "^4.1.3" + } } diff --git a/postcss.config.js b/postcss.config.js index e873f1a..12a703d 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,6 @@ module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/public/site.webmanifest b/public/site.webmanifest index 44266cb..dc0699d 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -1,14 +1,14 @@ { - "name": "Quice", - "short_name": "Quice", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" + "name": "Quice", + "short_name": "Quice", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" } diff --git a/scripts/seedDatabase.js b/scripts/seedDatabase.js index e4fc675..9f973e2 100644 --- a/scripts/seedDatabase.js +++ b/scripts/seedDatabase.js @@ -5,227 +5,227 @@ const { prompt } = require("enquirer"); const { subMonths } = require("date-fns"); admin.initializeApp({ - credential: admin.credential.cert({ - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, - privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"), - clientEmail: process.env.FIREBASE_CLIENT_EMAIL, - }), + credential: admin.credential.cert({ + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"), + clientEmail: process.env.FIREBASE_CLIENT_EMAIL, + }), }); const firestore = admin.firestore(); async function seed() { - const { testUserId } = await prompt({ - type: "input", - name: "testUserId", - message: "Enter the ID of the test user", - }); - - const forms = [ - { - name: "Sales Contact", - color: "orange", - owner: { type: "user", id: testUserId }, - }, - { - name: "Feedback (On-Site)", - color: "green", - owner: { type: "user", id: testUserId }, - }, - { - name: "General Contact", - color: "indigo", - owner: { type: "user", id: testUserId }, - }, - { - name: "Personal Get in Touch", - color: "pink", - owner: { type: "user", id: testUserId }, - }, - ]; - - const formBatch = firestore.batch(); - - const formCollection = firestore.collection("forms"); - - const formsWithIds = forms.map((form) => { - const newFormRef = formCollection.doc(); - - formBatch.set(newFormRef, form); - - return { ...form, id: newFormRef.id }; - }); - - await formBatch.commit(); - console.log("Successfully wrote form batch."); - - const submissionCollection = firestore.collection("submissions"); - - const today = new Date(); - - /** - * Sales Form, https://vercel.com/contact/sales - */ - const salesForm = formsWithIds.find((form) => form.color === "orange"); - - const salesBatch = firestore.batch(); - - const firstSalesSubmission = subMonths(today, 3); - - for (let i = 0; i < 200; i++) { - const submissionDate = getRandomDateInRange(firstSalesSubmission, today); - - const submission = { - createdAt: admin.firestore.Timestamp.fromDate(submissionDate), - formId: salesForm.id, - readAt: null, - data: { - email: faker.internet.email(), - name: `${faker.name.firstName()} ${faker.name.lastName()}`, - companyWebsite: faker.internet.url(), - companySize: faker.random.number(100), - productsOfInterest: [ - maybe() && "Vercel", - maybe() && "Preview Deployments", - maybe() && "Next.js", - maybe() && "Built in free SSL", - maybe() && "Edge Network", - maybe() && "Integrations", - maybe() && "Git Solutions", - maybe() && "Analytics / RES", - ].filter(Boolean), - howCanWeHelpYou: faker.lorem.sentences(), - }, - }; - - const newSubmissionRef = submissionCollection.doc(); - - salesBatch.set(newSubmissionRef, submission); - } - - await salesBatch.commit(); - console.log("Successfully wrote sales batch."); - - /** - * Feedback Form - * https://vercel.com/dashboard - */ - const feedbackForm = formsWithIds.find((form) => form.color === "green"); - - const feedbackBatch = firestore.batch(); - - const firstFeedbackSubmission = subMonths(today, 1); - - for (let i = 0; i < 300; i++) { - const submissionDate = getRandomDateInRange(firstFeedbackSubmission, today); - - const submission = { - createdAt: admin.firestore.Timestamp.fromDate(submissionDate), - formId: feedbackForm.id, - readAt: null, - data: { - feedback: faker.lorem.sentences(), - }, - }; - - const newSubmissionRef = submissionCollection.doc(); - - feedbackBatch.set(newSubmissionRef, submission); - } - - await feedbackBatch.commit(); - console.log("Successfully wrote feedback batch."); - - /** - * Contact Form - * https://labs.tobit.com/Kontakt - */ - const contactForm = formsWithIds.find((form) => form.color === "indigo"); - - const contactBatch = firestore.batch(); - - const firstContactSubmission = subMonths(today, 12); - - for (let i = 0; i < 120; i++) { - const submissionDate = getRandomDateInRange(firstContactSubmission, today); - - const submission = { - createdAt: admin.firestore.Timestamp.fromDate(submissionDate), - formId: contactForm.id, - readAt: null, - data: { - email: faker.internet.email(), - name: `${faker.name.firstName()} ${faker.name.lastName()}`, - companyWebsite: faker.internet.url(), - companySize: faker.random.number(100), - productsOfInterest: { - "Ich möchte mehr über das Digitalkonzept der Showcases erfahren": maybe(), - "Ich würde gerne die Labs besuchen": maybe(), - "Ich möchte die Digitalstadt Ahaus besichtigen": maybe(), - "Mich interessiert eine Kooperation": maybe(), - "Ich habe eine Frage zu Produkten": maybe(), - "Ich möchte den Campus für ein Event nutzen": maybe(), - "Ich möchte Feedback geben": maybe(), - "Ich habe eine Frage": maybe(), - }, - additional: maybe() ? faker.lorem.sentence() : "", - contact: maybe() ? faker.internet.email() : faker.phone.phoneNumber(), - }, - }; - - const newSubmissionRef = submissionCollection.doc(); - - contactBatch.set(newSubmissionRef, submission); - } - - await contactBatch.commit(); - console.log("Successfully wrote contact batch."); - - /** - * Personal Form - */ - const personalForm = formsWithIds.find((form) => form.color === "pink"); - - const personalBatch = firestore.batch(); - - const firstPersonalSubmission = subMonths(today, 5); - - for (let i = 0; i < 300; i++) { - const submissionDate = getRandomDateInRange(firstPersonalSubmission, today); - - const submission = { - createdAt: admin.firestore.Timestamp.fromDate(submissionDate), - formId: personalForm.id, - readAt: null, - data: { - firstName: faker.name.firstName(), - lastName: faker.name.lastName(), - company: faker.company.companyName(), - email: faker.internet.email(), - phoneNumber: faker.phone.phoneNumber(), - message: maybe() ? faker.lorem.sentences() : null, - agreed: true, - }, - }; - - const newSubmissionRef = submissionCollection.doc(); - - personalBatch.set(newSubmissionRef, submission); - } - - await personalBatch.commit(); - console.log("Successfully wrote personal batch."); + const { testUserId } = await prompt({ + type: "input", + name: "testUserId", + message: "Enter the ID of the test user", + }); + + const forms = [ + { + name: "Sales Contact", + color: "orange", + owner: { type: "user", id: testUserId }, + }, + { + name: "Feedback (On-Site)", + color: "green", + owner: { type: "user", id: testUserId }, + }, + { + name: "General Contact", + color: "indigo", + owner: { type: "user", id: testUserId }, + }, + { + name: "Personal Get in Touch", + color: "pink", + owner: { type: "user", id: testUserId }, + }, + ]; + + const formBatch = firestore.batch(); + + const formCollection = firestore.collection("forms"); + + const formsWithIds = forms.map((form) => { + const newFormRef = formCollection.doc(); + + formBatch.set(newFormRef, form); + + return { ...form, id: newFormRef.id }; + }); + + await formBatch.commit(); + console.log("Successfully wrote form batch."); + + const submissionCollection = firestore.collection("submissions"); + + const today = new Date(); + + /** + * Sales Form, https://vercel.com/contact/sales + */ + const salesForm = formsWithIds.find((form) => form.color === "orange"); + + const salesBatch = firestore.batch(); + + const firstSalesSubmission = subMonths(today, 3); + + for (let i = 0; i < 200; i++) { + const submissionDate = getRandomDateInRange(firstSalesSubmission, today); + + const submission = { + createdAt: admin.firestore.Timestamp.fromDate(submissionDate), + formId: salesForm.id, + readAt: null, + data: { + email: faker.internet.email(), + name: `${faker.name.firstName()} ${faker.name.lastName()}`, + companyWebsite: faker.internet.url(), + companySize: faker.random.number(100), + productsOfInterest: [ + maybe() && "Vercel", + maybe() && "Preview Deployments", + maybe() && "Next.js", + maybe() && "Built in free SSL", + maybe() && "Edge Network", + maybe() && "Integrations", + maybe() && "Git Solutions", + maybe() && "Analytics / RES", + ].filter(Boolean), + howCanWeHelpYou: faker.lorem.sentences(), + }, + }; + + const newSubmissionRef = submissionCollection.doc(); + + salesBatch.set(newSubmissionRef, submission); + } + + await salesBatch.commit(); + console.log("Successfully wrote sales batch."); + + /** + * Feedback Form + * https://vercel.com/dashboard + */ + const feedbackForm = formsWithIds.find((form) => form.color === "green"); + + const feedbackBatch = firestore.batch(); + + const firstFeedbackSubmission = subMonths(today, 1); + + for (let i = 0; i < 300; i++) { + const submissionDate = getRandomDateInRange(firstFeedbackSubmission, today); + + const submission = { + createdAt: admin.firestore.Timestamp.fromDate(submissionDate), + formId: feedbackForm.id, + readAt: null, + data: { + feedback: faker.lorem.sentences(), + }, + }; + + const newSubmissionRef = submissionCollection.doc(); + + feedbackBatch.set(newSubmissionRef, submission); + } + + await feedbackBatch.commit(); + console.log("Successfully wrote feedback batch."); + + /** + * Contact Form + * https://labs.tobit.com/Kontakt + */ + const contactForm = formsWithIds.find((form) => form.color === "indigo"); + + const contactBatch = firestore.batch(); + + const firstContactSubmission = subMonths(today, 12); + + for (let i = 0; i < 120; i++) { + const submissionDate = getRandomDateInRange(firstContactSubmission, today); + + const submission = { + createdAt: admin.firestore.Timestamp.fromDate(submissionDate), + formId: contactForm.id, + readAt: null, + data: { + email: faker.internet.email(), + name: `${faker.name.firstName()} ${faker.name.lastName()}`, + companyWebsite: faker.internet.url(), + companySize: faker.random.number(100), + productsOfInterest: { + "Ich möchte mehr über das Digitalkonzept der Showcases erfahren": maybe(), + "Ich würde gerne die Labs besuchen": maybe(), + "Ich möchte die Digitalstadt Ahaus besichtigen": maybe(), + "Mich interessiert eine Kooperation": maybe(), + "Ich habe eine Frage zu Produkten": maybe(), + "Ich möchte den Campus für ein Event nutzen": maybe(), + "Ich möchte Feedback geben": maybe(), + "Ich habe eine Frage": maybe(), + }, + additional: maybe() ? faker.lorem.sentence() : "", + contact: maybe() ? faker.internet.email() : faker.phone.phoneNumber(), + }, + }; + + const newSubmissionRef = submissionCollection.doc(); + + contactBatch.set(newSubmissionRef, submission); + } + + await contactBatch.commit(); + console.log("Successfully wrote contact batch."); + + /** + * Personal Form + */ + const personalForm = formsWithIds.find((form) => form.color === "pink"); + + const personalBatch = firestore.batch(); + + const firstPersonalSubmission = subMonths(today, 5); + + for (let i = 0; i < 300; i++) { + const submissionDate = getRandomDateInRange(firstPersonalSubmission, today); + + const submission = { + createdAt: admin.firestore.Timestamp.fromDate(submissionDate), + formId: personalForm.id, + readAt: null, + data: { + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + company: faker.company.companyName(), + email: faker.internet.email(), + phoneNumber: faker.phone.phoneNumber(), + message: maybe() ? faker.lorem.sentences() : null, + agreed: true, + }, + }; + + const newSubmissionRef = submissionCollection.doc(); + + personalBatch.set(newSubmissionRef, submission); + } + + await personalBatch.commit(); + console.log("Successfully wrote personal batch."); } seed(); function getRandomDateInRange(from, to) { - const fromTime = from.getTime(); - const toTime = to.getTime(); + const fromTime = from.getTime(); + const toTime = to.getTime(); - return new Date(fromTime + Math.random() * (toTime - fromTime)); + return new Date(fromTime + Math.random() * (toTime - fromTime)); } function maybe() { - return Math.random() > 0.5; + return Math.random() > 0.5; } diff --git a/src/components/ColorSelect.tsx b/src/components/ColorSelect.tsx index e03f2b4..b59935d 100644 --- a/src/components/ColorSelect.tsx +++ b/src/components/ColorSelect.tsx @@ -4,120 +4,120 @@ import React, { ReactElement } from "react"; import { FormColor } from "src/types/form"; const colors: FormColor[] = [ - "green", - "indigo", - "orange", - "pink", - "purple", - "teal", - "yellow", + "green", + "indigo", + "orange", + "pink", + "purple", + "teal", + "yellow", ]; interface Props { - open: boolean; - color: FormColor; + open: boolean; + color: FormColor; } export default function ColorSelect({ open, color }: Props): ReactElement { - return ( - <> - -
- - - {capitalizeFirstLetter(color)} - -
- - {/* Heroicon name: selector */} - - -
- - - {colors.map((color) => ( - - clsx( - "text-gray-900 cursor-default select-none relative py-2 pl-3 pr-9", - active && "bg-gray-200" - ) - } - > - {({ selected }) => ( - <> -
-
+ return ( + <> + +
+ + + {capitalizeFirstLetter(color)} + +
+ + {/* Heroicon name: selector */} + + +
+ + + {colors.map((color) => ( + + clsx( + "text-gray-900 cursor-default select-none relative py-2 pl-3 pr-9", + active && "bg-gray-200" + ) + } + > + {({ selected }) => ( + <> +
+
- {selected && ( - - {/* Heroicon name: check */} - - - )} - - )} -
- ))} -
-
- - ); + {selected && ( + + {/* Heroicon name: check */} + + + )} + + )} +
+ ))} +
+
+ + ); } function capitalizeFirstLetter(text: string) { - return text.charAt(0).toUpperCase() + text.substr(1); + return text.charAt(0).toUpperCase() + text.substr(1); } diff --git a/src/components/ListHeader.tsx b/src/components/ListHeader.tsx index 96eeafb..2c67798 100644 --- a/src/components/ListHeader.tsx +++ b/src/components/ListHeader.tsx @@ -1,23 +1,23 @@ import React, { ReactElement, ReactNode } from "react"; interface Props { - headline: string; - children: ReactNode; - action?: ReactNode; + headline: string; + children: ReactNode; + action?: ReactNode; } export default function ListHeader({ - children, - headline, - action, + children, + headline, + action, }: Props): ReactElement { - return ( -
-
-

{headline}

- {action} -
-

{children}

-
- ); + return ( +
+
+

{headline}

+ {action} +
+

{children}

+
+ ); } diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx index fc5f3d4..1af738f 100644 --- a/src/components/Logo.tsx +++ b/src/components/Logo.tsx @@ -1,19 +1,19 @@ import React, { ReactElement, SVGProps } from "react"; export default function Logo(props: SVGProps): ReactElement { - return ( - - - - ); + return ( + + + + ); } diff --git a/src/components/ModalProvider.tsx b/src/components/ModalProvider.tsx index ec537db..94dfb48 100644 --- a/src/components/ModalProvider.tsx +++ b/src/components/ModalProvider.tsx @@ -1,21 +1,21 @@ import { Transition } from "@headlessui/react"; import OutlineExclamationIcon from "heroicons/outline/exclamation.svg"; import { - createContext, - ReactElement, - ReactNode, - useCallback, - useContext, - useState, + createContext, + ReactElement, + ReactNode, + useCallback, + useContext, + useState, } from "react"; type DialogType = "destructive"; interface DialogOptions { - type: DialogType; - title: string; - text: string; - confirmText: string; + type: DialogType; + title: string; + text: string; + confirmText: string; } type ModalContextValue = (options: DialogOptions) => Promise; @@ -23,127 +23,127 @@ type ModalContextValue = (options: DialogOptions) => Promise; const ModalContext = createContext(null); export function useModal() { - const ctx = useContext(ModalContext); + const ctx = useContext(ModalContext); - if (ctx === null) throw Error("No ModalContext found."); + if (ctx === null) throw Error("No ModalContext found."); - return ctx; + return ctx; } interface Props { - children: ReactNode; + children: ReactNode; } type State = - | ({ - state: "closed"; - resolveFn?: (value: boolean) => void; - } & Partial) - | ({ - state: "open"; - resolveFn: (value: boolean) => void; - } & DialogOptions); + | ({ + state: "closed"; + resolveFn?: (value: boolean) => void; + } & Partial) + | ({ + state: "open"; + resolveFn: (value: boolean) => void; + } & DialogOptions); export default function ModalProvider({ children }: Props): ReactElement { - const [dialogState, setDialogState] = useState({ state: "closed" }); + const [dialogState, setDialogState] = useState({ state: "closed" }); - const openModal = useCallback( - ({ text, title, type, confirmText }: DialogOptions) => - new Promise((resolveFn) => { - setDialogState({ - state: "open", - text, - title, - type, - confirmText, - resolveFn, - }); - }), - [] - ); + const openModal = useCallback( + ({ text, title, type, confirmText }: DialogOptions) => + new Promise((resolveFn) => { + setDialogState({ + state: "open", + text, + title, + type, + confirmText, + resolveFn, + }); + }), + [] + ); - function handleClose(result: boolean) { - dialogState.resolveFn?.(result); - setDialogState((state) => ({ ...state, state: "closed" })); - } + function handleClose(result: boolean) { + dialogState.resolveFn?.(result); + setDialogState((state) => ({ ...state, state: "closed" })); + } - return ( - - {children} -
-
-
-
-
- -
-
- {/* Description list */} -
-
- {submission && } -
-
-
- ); + showSnackbar("URL copied to clipboard."); + }} + > +
+
+ + + + {/* Description list */} +
+
+ {submission && } +
+
+ + ); } diff --git a/src/components/SubmissionList.tsx b/src/components/SubmissionList.tsx index 95cea37..06f694b 100644 --- a/src/components/SubmissionList.tsx +++ b/src/components/SubmissionList.tsx @@ -1,13 +1,13 @@ import { - differenceInDays, - differenceInHours, - differenceInMinutes, - differenceInSeconds, - format, - isSameDay, - isThisYear, - isToday, - isYesterday, + differenceInDays, + differenceInHours, + differenceInMinutes, + differenceInSeconds, + format, + isSameDay, + isThisYear, + isToday, + isYesterday, } from "date-fns"; import { ReactElement, useMemo } from "react"; import { GroupedVirtuoso } from "react-virtuoso"; @@ -16,181 +16,181 @@ import { FormSubmission } from "src/types/form"; import Spinner from "./Spinner"; interface Props { - submissions?: FormSubmission[]; - canLoadMore: boolean; - loadMore: () => void; - onSelect: (submissionId: string) => void; + submissions?: FormSubmission[]; + canLoadMore: boolean; + loadMore: () => void; + onSelect: (submissionId: string) => void; } export default function SubmissionList({ - canLoadMore, - loadMore, - submissions, - onSelect, + canLoadMore, + loadMore, + submissions, + onSelect, }: Props): ReactElement { - const { groupCounts, groups } = useMemo(() => { - if (!submissions) { - return { - groupCounts: undefined, - groups: undefined, - }; - } - - const buckets: Array<{ - type: "last-hour" | "day"; - day?: Date; - submissions: FormSubmission[]; - }> = []; - - submissions.forEach((submission) => { - const submissionDate = submission.createdAt.toDate(); - const now = Date.now(); - - if (differenceInHours(now, submissionDate) < 1) { - const lastHourBucket = buckets.find( - (bucket) => bucket.type === "last-hour" - ); - - if (lastHourBucket) { - lastHourBucket.submissions.push(submission); - } else { - buckets.unshift({ type: "last-hour", submissions: [submission] }); - } - } else { - const dateCopy = new Date(submissionDate); - dateCopy.setUTCHours(0); - dateCopy.setUTCMinutes(0); - dateCopy.setUTCSeconds(0); - dateCopy.setUTCMilliseconds(0); - - const dayBucket = buckets.find( - (bucket) => bucket.day && isSameDay(bucket.day, dateCopy) - ); - - if (dayBucket) { - dayBucket.submissions.push(submission); - } else { - buckets.push({ - type: "day", - day: dateCopy, - submissions: [submission], - }); - } - } - }); - - return { - groupCounts: buckets.map((bucket) => bucket.submissions.length), - groups: buckets.map((bucket) => { - if (bucket.type === "last-hour") { - return "Last hour"; - } - - if (!bucket.day) throw Error("Invalid bucket."); - - if (isToday(bucket.day)) return "Today"; - if (isYesterday(bucket.day)) return "Yesterday"; - if (isThisYear(bucket.day)) return format(bucket.day, "MMMM d"); - - return format(bucket.day, "MMMM d, yyy"); - }), - }; - }, [submissions]); - - if (!submissions || !groups) - return ( -
- -
- ); - - return ( - - ); + const { groupCounts, groups } = useMemo(() => { + if (!submissions) { + return { + groupCounts: undefined, + groups: undefined, + }; + } + + const buckets: Array<{ + type: "last-hour" | "day"; + day?: Date; + submissions: FormSubmission[]; + }> = []; + + submissions.forEach((submission) => { + const submissionDate = submission.createdAt.toDate(); + const now = Date.now(); + + if (differenceInHours(now, submissionDate) < 1) { + const lastHourBucket = buckets.find( + (bucket) => bucket.type === "last-hour" + ); + + if (lastHourBucket) { + lastHourBucket.submissions.push(submission); + } else { + buckets.unshift({ type: "last-hour", submissions: [submission] }); + } + } else { + const dateCopy = new Date(submissionDate); + dateCopy.setUTCHours(0); + dateCopy.setUTCMinutes(0); + dateCopy.setUTCSeconds(0); + dateCopy.setUTCMilliseconds(0); + + const dayBucket = buckets.find( + (bucket) => bucket.day && isSameDay(bucket.day, dateCopy) + ); + + if (dayBucket) { + dayBucket.submissions.push(submission); + } else { + buckets.push({ + type: "day", + day: dateCopy, + submissions: [submission], + }); + } + } + }); + + return { + groupCounts: buckets.map((bucket) => bucket.submissions.length), + groups: buckets.map((bucket) => { + if (bucket.type === "last-hour") { + return "Last hour"; + } + + if (!bucket.day) throw Error("Invalid bucket."); + + if (isToday(bucket.day)) return "Today"; + if (isYesterday(bucket.day)) return "Yesterday"; + if (isThisYear(bucket.day)) return format(bucket.day, "MMMM d"); + + return format(bucket.day, "MMMM d, yyy"); + }), + }; + }, [submissions]); + + if (!submissions || !groups) + return ( +
+ +
+ ); + + return ( + + ); } diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index 0702372..b5c15be 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -2,74 +2,74 @@ import clsx from "clsx"; import React, { ReactElement } from "react"; interface Props { - value: boolean; - onChange: (newValue: boolean) => void; - label: string; + value: boolean; + onChange: (newValue: boolean) => void; + label: string; } export default function Switch({ - onChange, - value, - label, + onChange, + value, + label, }: Props): ReactElement { - return ( - - ); + return ( + + ); } diff --git a/src/components/form-data-list/ArrayEntry.tsx b/src/components/form-data-list/ArrayEntry.tsx index 802e6e4..544c456 100644 --- a/src/components/form-data-list/ArrayEntry.tsx +++ b/src/components/form-data-list/ArrayEntry.tsx @@ -2,18 +2,18 @@ import React, { ReactElement } from "react"; import { JsonArray } from "type-fest"; interface Props { - propertyName: string; - value: JsonArray; + propertyName: string; + value: JsonArray; } export default function ArrayEntry({ - propertyName, - value, + propertyName, + value, }: Props): ReactElement { - return ( -
-
{propertyName}
-
{value}
-
- ); + return ( +
+
{propertyName}
+
{value}
+
+ ); } diff --git a/src/components/form-data-list/BooleanEntry.tsx b/src/components/form-data-list/BooleanEntry.tsx index 8270d38..7e3bf1f 100644 --- a/src/components/form-data-list/BooleanEntry.tsx +++ b/src/components/form-data-list/BooleanEntry.tsx @@ -3,33 +3,33 @@ import SolidXIcon from "heroicons/solid/x.svg"; import React, { ReactElement } from "react"; interface Props { - propertyName: string; - value: boolean; + propertyName: string; + value: boolean; } export default function BooleanEntry({ - propertyName, - value, + propertyName, + value, }: Props): ReactElement { - if (value) { - return ( -
-
{propertyName}
-
- True -
-
- ); - } + if (value) { + return ( +
+
{propertyName}
+
+ True +
+
+ ); + } - return ( -
-
{propertyName}
-
- False -
-
- ); + return ( +
+
{propertyName}
+
+ False +
+
+ ); } diff --git a/src/components/form-data-list/DataEntry.tsx b/src/components/form-data-list/DataEntry.tsx index 7adb3cb..f511339 100644 --- a/src/components/form-data-list/DataEntry.tsx +++ b/src/components/form-data-list/DataEntry.tsx @@ -9,37 +9,37 @@ import ObjectEntry from "./ObjectEntry"; import StringEntry from "./StringEntry"; interface DataEntryProps { - propertyName: string; - value: JsonValue; + propertyName: string; + value: JsonValue; } export default function DataEntry({ - propertyName, - value, + propertyName, + value, }: DataEntryProps): ReactElement { - const prettyName = useMemo(() => prettifyPropertyName(propertyName), [ - propertyName, - ]); + const prettyName = useMemo(() => prettifyPropertyName(propertyName), [ + propertyName, + ]); - switch (typeof value) { - case "string": - return ; - case "boolean": - return ; - case "number": - return ; - case "object": - if (value === null) { - return ; - } + switch (typeof value) { + case "string": + return ; + case "boolean": + return ; + case "number": + return ; + case "object": + if (value === null) { + return ; + } - if (Array.isArray(value)) { - return ; - } + if (Array.isArray(value)) { + return ; + } - return ; + return ; - default: - throw Error("Unknown data type for value."); - } + default: + throw Error("Unknown data type for value."); + } } diff --git a/src/components/form-data-list/DataList.tsx b/src/components/form-data-list/DataList.tsx index 3f0ea7b..8efddb5 100644 --- a/src/components/form-data-list/DataList.tsx +++ b/src/components/form-data-list/DataList.tsx @@ -3,30 +3,30 @@ import { JsonObject } from "type-fest"; import DataEntry from "./DataEntry"; interface Props { - data: JsonObject; + data: JsonObject; } export default function DataList({ data }: Props): ReactElement { - const sortedDataEntries = useMemo(() => { - const entries = Object.entries(data); + const sortedDataEntries = useMemo(() => { + const entries = Object.entries(data); - /** - * Sorts the data entries by name. - */ - return Array.from(entries).sort((a, b) => { - if (a[0] < b[0]) return -1; - if (a[0] > b[0]) return 1; - return 0; - }); - }, [data]); + /** + * Sorts the data entries by name. + */ + return Array.from(entries).sort((a, b) => { + if (a[0] < b[0]) return -1; + if (a[0] > b[0]) return 1; + return 0; + }); + }, [data]); - return ( - <> - {sortedDataEntries.map(([key, value]) => { - if (value === undefined) return null; + return ( + <> + {sortedDataEntries.map(([key, value]) => { + if (value === undefined) return null; - return ; - })} - - ); + return ; + })} + + ); } diff --git a/src/components/form-data-list/NullEntry.tsx b/src/components/form-data-list/NullEntry.tsx index 128cbe5..ce99223 100644 --- a/src/components/form-data-list/NullEntry.tsx +++ b/src/components/form-data-list/NullEntry.tsx @@ -1,14 +1,14 @@ import React, { ReactElement } from "react"; interface Props { - propertyName: string; + propertyName: string; } export default function NullEntry({ propertyName }: Props): ReactElement { - return ( -
-
{propertyName}
-
-
-
- ); + return ( +
+
{propertyName}
+
-
+
+ ); } diff --git a/src/components/form-data-list/NumberEntry.tsx b/src/components/form-data-list/NumberEntry.tsx index 704d52b..8794571 100644 --- a/src/components/form-data-list/NumberEntry.tsx +++ b/src/components/form-data-list/NumberEntry.tsx @@ -1,18 +1,18 @@ import React, { ReactElement } from "react"; interface Props { - propertyName: string; - value: number; + propertyName: string; + value: number; } export default function NumberEntry({ - propertyName, - value, + propertyName, + value, }: Props): ReactElement { - return ( -
-
{propertyName}
-
{value.toLocaleString()}
-
- ); + return ( +
+
{propertyName}
+
{value.toLocaleString()}
+
+ ); } diff --git a/src/components/form-data-list/ObjectEntry.tsx b/src/components/form-data-list/ObjectEntry.tsx index f22fe7c..8471632 100644 --- a/src/components/form-data-list/ObjectEntry.tsx +++ b/src/components/form-data-list/ObjectEntry.tsx @@ -2,20 +2,20 @@ import React, { ReactElement } from "react"; import { JsonObject } from "type-fest"; interface Props { - propertyName: string; - value: JsonObject; + propertyName: string; + value: JsonObject; } export default function ObjectEntry({ - propertyName, - value, + propertyName, + value, }: Props): ReactElement { - return ( -
-
{propertyName}
-
-
{JSON.stringify(value, null, 2)}
-
-
- ); + return ( +
+
{propertyName}
+
+
{JSON.stringify(value, null, 2)}
+
+
+ ); } diff --git a/src/components/form-data-list/StringEntry.tsx b/src/components/form-data-list/StringEntry.tsx index 370a6ac..3ab16c9 100644 --- a/src/components/form-data-list/StringEntry.tsx +++ b/src/components/form-data-list/StringEntry.tsx @@ -2,40 +2,40 @@ import React, { ReactElement } from "react"; import Linkify from "react-linkify"; interface Props { - propertyName: string; - value: string; + propertyName: string; + value: string; } export default function StringEntry({ - propertyName, - value, + propertyName, + value, }: Props): ReactElement { - let displayValue = value.trim(); + let displayValue = value.trim(); - if (displayValue === "") { - displayValue = "-"; - } + if (displayValue === "") { + displayValue = "-"; + } - return ( -
-
{propertyName}
-
- ( - - {text.toLowerCase()} - - )} - > - {displayValue} - -
-
- ); + return ( +
+
{propertyName}
+
+ ( + + {text.toLowerCase()} + + )} + > + {displayValue} + +
+
+ ); } diff --git a/src/components/sidebar/MenuLink.tsx b/src/components/sidebar/MenuLink.tsx index 3f0368e..421376d 100644 --- a/src/components/sidebar/MenuLink.tsx +++ b/src/components/sidebar/MenuLink.tsx @@ -4,39 +4,39 @@ import { useRouter } from "next/router"; import React, { ComponentType, ReactElement, SVGProps } from "react"; interface Props { - href: string; - Icon: ComponentType>; - children: string; + href: string; + Icon: ComponentType>; + children: string; } export default function MenuLink({ - children, - href, - Icon, + children, + href, + Icon, }: Props): ReactElement { - const router = useRouter(); + const router = useRouter(); - const isSelected = router.asPath.startsWith(href); + const isSelected = router.asPath.startsWith(href); - return ( - - - - - ); + return ( + + + + + ); } diff --git a/src/components/sidebar/SecondaryLink.tsx b/src/components/sidebar/SecondaryLink.tsx index c97ddc0..0596738 100644 --- a/src/components/sidebar/SecondaryLink.tsx +++ b/src/components/sidebar/SecondaryLink.tsx @@ -4,34 +4,34 @@ import { useRouter } from "next/router"; import React, { ReactElement, ReactNode } from "react"; interface Props { - href: string; - children: ReactNode; - leading: ReactNode; + href: string; + children: ReactNode; + leading: ReactNode; } export default function SecondaryLink({ - href, - leading, - children, + href, + leading, + children, }: Props): ReactElement { - const router = useRouter(); + const router = useRouter(); - const isActive = router.asPath.startsWith(href); + const isActive = router.asPath.startsWith(href); - return ( - - - {leading} - {children} - - - ); + return ( + + + {leading} + {children} + + + ); } diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index 7434f1a..0fd9fa0 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -12,232 +12,232 @@ import MenuLink from "./MenuLink"; import SecondaryLink from "./SecondaryLink"; export default function Sidebar(): ReactElement { - const { user, signout } = useAuth(); + const { user, signout } = useAuth(); - const [showDropdown, setShowDropdown] = useState(false); - const [searchString, setSearchString] = useState(""); + const [showDropdown, setShowDropdown] = useState(false); + const [searchString, setSearchString] = useState(""); - const [forms, loading, error] = useCollectionData
( - user ? firestore.collection("forms").orderBy("name") : null - ); + const [forms, loading, error] = useCollectionData( + user ? firestore.collection("forms").orderBy("name") : null + ); - return ( -
-
- -

- Quice -

-
- {/* User account dropdown */} -
- {/* Dropdown menu toggle, controlling the show/hide state of dropdown menu. */} -
- -
- {/* Dropdown panel, show/hide based on dropdown state. */} - - - -
- -
-
-
+ return ( +
+
+ +

+ Quice +

+
+ {/* User account dropdown */} +
+ {/* Dropdown menu toggle, controlling the show/hide state of dropdown menu. */} +
+ +
+ {/* Dropdown panel, show/hide based on dropdown state. */} + + + +
+ +
+
+
- {/* Navigation */} -
+ +
+ ); } diff --git a/src/firebase/client.ts b/src/firebase/client.ts index d83235e..ee561bf 100644 --- a/src/firebase/client.ts +++ b/src/firebase/client.ts @@ -3,14 +3,14 @@ import "firebase/auth"; import "firebase/firestore"; const clientCredentials = { - apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, - appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, - authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, }; if (!firebase.apps.length) { - firebase.initializeApp(clientCredentials); + firebase.initializeApp(clientCredentials); } export { firebase }; diff --git a/src/firebase/infiniteQuery.ts b/src/firebase/infiniteQuery.ts index 7dd7a4f..b16fa9d 100644 --- a/src/firebase/infiniteQuery.ts +++ b/src/firebase/infiniteQuery.ts @@ -4,78 +4,78 @@ import { FormSubmission } from "src/types/form"; import { firebase } from "./client"; export function useSubmissionQuery( - orderedQuery: firebase.firestore.Query | null, - chunkSize = 20 + orderedQuery: firebase.firestore.Query | null, + chunkSize = 20 ) { - const subscribtions = useRef void>>([]); - - const [docs, setDocs] = useState(); - const [canLoadMore, setCanLoadMore] = useState(true); - - const snapshotHandler = useCallback( - ( - snapshot: firebase.firestore.QuerySnapshot - ) => { - if (snapshot.size === 0) setCanLoadMore(false); - - setDocs((docs) => { - const map = new Map(); - - docs?.forEach((doc) => map.set(doc.id, doc)); - snapshot.docs.forEach((doc) => - map.set(doc.id, { id: doc.id, ...doc.data() }) - ); - - const newDocs = Array.from(map.values()) as FormSubmission[]; - newDocs.sort((a, b) => { - return b.createdAt.toMillis() - a.createdAt.toMillis(); - }); - - return newDocs; - }); - }, - [] - ); - - const queryCached = useMemoCompare(orderedQuery, (prevQuery) => { - return Boolean( - prevQuery && orderedQuery && orderedQuery.isEqual(prevQuery) - ); - }); - - useEffect(() => { - if (!queryCached) return; - - const unsubscribe = queryCached - .limit(chunkSize) - .onSnapshot(snapshotHandler); - - subscribtions.current.push(unsubscribe); - }, [chunkSize, queryCached, snapshotHandler]); - - useEffect(function unsubscribeAll() { - return () => { - // eslint-disable-next-line react-hooks/exhaustive-deps -- We do not really care when it changes, since we want to end all subscribtions anyway. - return subscribtions.current.forEach((unsubscribe) => unsubscribe()); - }; - }, []); - - function loadMore() { - if (!queryCached) return; - - const lastDoc = docs?.[docs.length - 1]; - - const unsubscribe = queryCached - .limit(chunkSize) - .startAfter(lastDoc?.createdAt) - .onSnapshot(snapshotHandler, console.error); - - subscribtions.current.push(unsubscribe); - } - - return { - submissions: docs, - canLoadMore, - loadMore, - }; + const subscribtions = useRef void>>([]); + + const [docs, setDocs] = useState(); + const [canLoadMore, setCanLoadMore] = useState(true); + + const snapshotHandler = useCallback( + ( + snapshot: firebase.firestore.QuerySnapshot + ) => { + if (snapshot.size === 0) setCanLoadMore(false); + + setDocs((docs) => { + const map = new Map(); + + docs?.forEach((doc) => map.set(doc.id, doc)); + snapshot.docs.forEach((doc) => + map.set(doc.id, { id: doc.id, ...doc.data() }) + ); + + const newDocs = Array.from(map.values()) as FormSubmission[]; + newDocs.sort((a, b) => { + return b.createdAt.toMillis() - a.createdAt.toMillis(); + }); + + return newDocs; + }); + }, + [] + ); + + const queryCached = useMemoCompare(orderedQuery, (prevQuery) => { + return Boolean( + prevQuery && orderedQuery && orderedQuery.isEqual(prevQuery) + ); + }); + + useEffect(() => { + if (!queryCached) return; + + const unsubscribe = queryCached + .limit(chunkSize) + .onSnapshot(snapshotHandler); + + subscribtions.current.push(unsubscribe); + }, [chunkSize, queryCached, snapshotHandler]); + + useEffect(function unsubscribeAll() { + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps -- We do not really care when it changes, since we want to end all subscribtions anyway. + return subscribtions.current.forEach((unsubscribe) => unsubscribe()); + }; + }, []); + + function loadMore() { + if (!queryCached) return; + + const lastDoc = docs?.[docs.length - 1]; + + const unsubscribe = queryCached + .limit(chunkSize) + .startAfter(lastDoc?.createdAt) + .onSnapshot(snapshotHandler, console.error); + + subscribtions.current.push(unsubscribe); + } + + return { + submissions: docs, + canLoadMore, + loadMore, + }; } diff --git a/src/hooks/memoCompare.ts b/src/hooks/memoCompare.ts index 8103694..04a9003 100644 --- a/src/hooks/memoCompare.ts +++ b/src/hooks/memoCompare.ts @@ -1,19 +1,19 @@ import { useEffect, useRef } from "react"; export function useMemoCompare( - next: T, - compare: (previous: T | undefined, next?: T) => boolean + next: T, + compare: (previous: T | undefined, next?: T) => boolean ): T { - const previousRef = useRef(); - const previous = previousRef.current; + const previousRef = useRef(); + const previous = previousRef.current; - const isEqual = compare(previous, next); + const isEqual = compare(previous, next); - useEffect(() => { - if (!isEqual) { - previousRef.current = next; - } - }); + useEffect(() => { + if (!isEqual) { + previousRef.current = next; + } + }); - return isEqual && previous !== undefined ? previous : next; + return isEqual && previous !== undefined ? previous : next; } diff --git a/src/hooks/useHost.ts b/src/hooks/useHost.ts index 2dc2fa2..5106594 100644 --- a/src/hooks/useHost.ts +++ b/src/hooks/useHost.ts @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; export function useHost(): string { - const [host, setHost] = useState(""); + const [host, setHost] = useState(""); - useEffect(() => { - setHost(window.location.host); - }, []); + useEffect(() => { + setHost(window.location.host); + }, []); - return host; + return host; } diff --git a/src/layouts/AppLayout.tsx b/src/layouts/AppLayout.tsx index 133884d..c1dd76f 100644 --- a/src/layouts/AppLayout.tsx +++ b/src/layouts/AppLayout.tsx @@ -6,91 +6,91 @@ import Logo from "../components/Logo"; import Sidebar from "../components/sidebar/Sidebar"; interface Props { - children: ReactNode; + children: ReactNode; } export function AppLayout({ children }: Props): ReactElement { - const [showDrawer, setShowDrawer] = useState(false); + const [showDrawer, setShowDrawer] = useState(false); - return ( -
- {/* Off-canvas menu for mobile, show/hide based on off-canvas menu state. */} -
- - - {/* eslint-disable-next-line -- Is not accessible, but doesn't have to, since there is a dedicated close button. */} -
setShowDrawer(false)} - /> - + return ( +
+ {/* Off-canvas menu for mobile, show/hide based on off-canvas menu state. */} +
+ + + {/* eslint-disable-next-line -- Is not accessible, but doesn't have to, since there is a dedicated close button. */} +
setShowDrawer(false)} + /> + - -
- -
- -
- - -
- {/* Static sidebar for desktop */} -
-
- -
-
-
-
-
-
- -
-
- -
-
-
-
- {children} -
-
-
- ); + +
+ +
+ +
+ + +
+ {/* Static sidebar for desktop */} +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+ {children} +
+
+
+ ); } diff --git a/src/layouts/AuthLayout.tsx b/src/layouts/AuthLayout.tsx index 7a4ac3f..2c5975f 100644 --- a/src/layouts/AuthLayout.tsx +++ b/src/layouts/AuthLayout.tsx @@ -2,32 +2,32 @@ import { ReactElement, ReactNode } from "react"; import Logo from "../components/Logo"; interface Props { - headline: string; - subheadline: ReactNode; - children: ReactNode; + headline: string; + subheadline: ReactNode; + children: ReactNode; } export function AuthLayout({ - children, - headline, - subheadline, + children, + headline, + subheadline, }: Props): ReactElement { - return ( -
-
- -

- {headline} -

-

- {subheadline} -

-
-
-
- {children} -
-
-
- ); + return ( +
+
+ +

+ {headline} +

+

+ {subheadline} +

+
+
+
+ {children} +
+
+
+ ); } diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx index 81908c9..25265ae 100644 --- a/src/lib/auth.tsx +++ b/src/lib/auth.tsx @@ -1,139 +1,139 @@ import { useRouter } from "next/router"; import React, { - createContext, - ReactNode, - useContext, - useEffect, - useState, + createContext, + ReactNode, + useContext, + useEffect, + useState, } from "react"; import { firebase, firestore } from "../firebase/client"; interface SignupInfo { - email: string; - password: string; - firstName: string; - lastName: string; + email: string; + password: string; + firstName: string; + lastName: string; } interface AuthContextValue { - user: firebase.User | null; + user: firebase.User | null; - signin: ( - email: string, - password: string, - redirect?: string - ) => Promise; + signin: ( + email: string, + password: string, + redirect?: string + ) => Promise; - signup: ( - info: SignupInfo, - redirect?: string - ) => Promise; + signup: ( + info: SignupInfo, + redirect?: string + ) => Promise; - signout: () => Promise; + signout: () => Promise; - sendPasswordResetEmail: (email: string) => Promise; + sendPasswordResetEmail: (email: string) => Promise; - confirmPasswordReset: (code: string, password: string) => Promise; + confirmPasswordReset: (code: string, password: string) => Promise; } const AuthContext = createContext(null); export function ProvideAuth({ children }: { children: ReactNode }) { - const auth = useProvideAuth(); + const auth = useProvideAuth(); - return {children}; + return {children}; } export function useAuth() { - const value = useContext(AuthContext); + const value = useContext(AuthContext); - if (value === null) { - throw Error("AuthContext not found."); - } + if (value === null) { + throw Error("AuthContext not found."); + } - return value; + return value; } function useProvideAuth(): AuthContextValue { - const [user, setUser] = useState(null); - const router = useRouter(); - - async function signin(email: string, password: string, redirect?: string) { - const response = await firebase - .auth() - .signInWithEmailAndPassword(email, password); - - setUser(response.user); - - if (redirect) { - router.push(redirect); - } - - return response.user; - } - - async function signup( - { email, password, firstName, lastName }: SignupInfo, - redirect?: string - ) { - const response = await firebase - .auth() - .createUserWithEmailAndPassword(email, password); - - const { user } = response; - - if (user) { - await firestore.collection("users").doc(user.uid).set({ - firstName, - lastName, - emailAddress: user.email, - }); - } - - setUser(response.user); - - if (redirect) { - router.push(redirect); - } - - return response.user; - } - - async function signout() { - await firebase.auth().signOut(); - - setUser(null); - router.push("/"); - } - - async function sendPasswordResetEmail(email: string) { - await firebase.auth().sendPasswordResetEmail(email); - } - - async function confirmPasswordReset(code: string, password: string) { - await firebase.auth().confirmPasswordReset(code, password); - } - - useEffect(function addUserSubscription() { - const unsubscribe = firebase.auth().onAuthStateChanged((user) => { - if (user) { - setUser(user); - } else { - setUser(null); - } - }); - - return () => { - unsubscribe(); - }; - }, []); - - return { - user, - signin, - signup, - signout, - sendPasswordResetEmail, - confirmPasswordReset, - }; + const [user, setUser] = useState(null); + const router = useRouter(); + + async function signin(email: string, password: string, redirect?: string) { + const response = await firebase + .auth() + .signInWithEmailAndPassword(email, password); + + setUser(response.user); + + if (redirect) { + router.push(redirect); + } + + return response.user; + } + + async function signup( + { email, password, firstName, lastName }: SignupInfo, + redirect?: string + ) { + const response = await firebase + .auth() + .createUserWithEmailAndPassword(email, password); + + const { user } = response; + + if (user) { + await firestore.collection("users").doc(user.uid).set({ + firstName, + lastName, + emailAddress: user.email, + }); + } + + setUser(response.user); + + if (redirect) { + router.push(redirect); + } + + return response.user; + } + + async function signout() { + await firebase.auth().signOut(); + + setUser(null); + router.push("/"); + } + + async function sendPasswordResetEmail(email: string) { + await firebase.auth().sendPasswordResetEmail(email); + } + + async function confirmPasswordReset(code: string, password: string) { + await firebase.auth().confirmPasswordReset(code, password); + } + + useEffect(function addUserSubscription() { + const unsubscribe = firebase.auth().onAuthStateChanged((user) => { + if (user) { + setUser(user); + } else { + setUser(null); + } + }); + + return () => { + unsubscribe(); + }; + }, []); + + return { + user, + signin, + signup, + signout, + sendPasswordResetEmail, + confirmPasswordReset, + }; } diff --git a/src/lib/submissionTitle.ts b/src/lib/submissionTitle.ts index 8b97a44..39a0d41 100644 --- a/src/lib/submissionTitle.ts +++ b/src/lib/submissionTitle.ts @@ -1,26 +1,26 @@ import { FormSubmission } from "src/types/form"; export function getSubmissionTitle({ data, id }: FormSubmission) { - if (typeof data.title === "string") return data.title; + if (typeof data.title === "string") return data.title; - if (typeof data.company === "string") return data.company; - if (typeof data.companyName === "string") return data.companyName; + if (typeof data.company === "string") return data.company; + if (typeof data.companyName === "string") return data.companyName; - if (typeof data.name === "string") return data.name; + if (typeof data.name === "string") return data.name; - if (typeof data.firstName === "string") { - if (typeof data.lastName === "string") { - return `${data.firstName} ${data.lastName}`; - } + if (typeof data.firstName === "string") { + if (typeof data.lastName === "string") { + return `${data.firstName} ${data.lastName}`; + } - return data.firstName; - } + return data.firstName; + } - const stringProp = Object.values(data).find( - (value) => typeof value === "string" - ) as string; + const stringProp = Object.values(data).find( + (value) => typeof value === "string" + ) as string; - if (stringProp) return stringProp; + if (stringProp) return stringProp; - return id; + return id; } diff --git a/src/lib/validateName.ts b/src/lib/validateName.ts index 74f1c1a..a1c6c74 100644 --- a/src/lib/validateName.ts +++ b/src/lib/validateName.ts @@ -2,22 +2,22 @@ import kebabCase from "lodash.kebabcase"; import { firestore } from "src/firebase/client"; export async function validateName(name: string) { - const slug = kebabCase(name); + const slug = kebabCase(name); - if (invalidSlugs.includes(slug)) { - return "This URL is reserved, please choose another name."; - } + if (invalidSlugs.includes(slug)) { + return "This URL is reserved, please choose another name."; + } - const formWithSameSlug = await firestore - .collection("forms") - .where("slug", "==", slug) - .get(); + const formWithSameSlug = await firestore + .collection("forms") + .where("slug", "==", slug) + .get(); - if (formWithSameSlug.size > 0) { - return "A form with this URL already exists."; - } + if (formWithSameSlug.size > 0) { + return "A form with this URL already exists."; + } - return true; + return true; } const invalidSlugs = ["new-form", "login", "join", "inbox", "tasks"]; diff --git a/src/pages/[formSlug]/[submissionId].tsx b/src/pages/[formSlug]/[submissionId].tsx index b9a3389..d89db8a 100644 --- a/src/pages/[formSlug]/[submissionId].tsx +++ b/src/pages/[formSlug]/[submissionId].tsx @@ -12,97 +12,97 @@ import { AppLayout } from "src/layouts/AppLayout"; import { Form } from "src/types/form"; export default function FormPage() { - const router = useRouter(); + const router = useRouter(); - const { formSlug, submissionId } = router.query; + const { formSlug, submissionId } = router.query; - const [forms, loading, error] = useCollectionData( - formSlug - ? firestore.collection("forms").where("slug", "==", formSlug) - : null - ); + const [forms, loading, error] = useCollectionData( + formSlug + ? firestore.collection("forms").where("slug", "==", formSlug) + : null + ); - const form = forms?.[0]; + const form = forms?.[0]; - const { canLoadMore, loadMore, submissions } = useSubmissionQuery( - form - ? firestore - .collection("submissions") - .where("formId", "==", form.id) - .orderBy("createdAt", "desc") - : null - ); + const { canLoadMore, loadMore, submissions } = useSubmissionQuery( + form + ? firestore + .collection("submissions") + .where("formId", "==", form.id) + .orderBy("createdAt", "desc") + : null + ); - const selectedSubmission = - submissions && submissionId - ? submissions.find((submission) => submission.id === submissionId) - : null; + const selectedSubmission = + submissions && submissionId + ? submissions.find((submission) => submission.id === submissionId) + : null; - return ( - <> -
- {/* Breadcrumb */} - - -
- - - ); + return ( + <> +
+ {/* Breadcrumb */} + + +
+ + + ); } FormPage.getLayout = (page: ReactNode) => { - return {page}; + return {page}; }; diff --git a/src/pages/[formSlug]/settings.tsx b/src/pages/[formSlug]/settings.tsx index c32d3b8..be0e985 100644 --- a/src/pages/[formSlug]/settings.tsx +++ b/src/pages/[formSlug]/settings.tsx @@ -21,126 +21,126 @@ import { Form, FormColor } from "src/types/form"; SyntaxHighlighter.registerLanguage("javascript", js); interface CreateFormForm { - name: string; - description: string; - color: FormColor; + name: string; + description: string; + color: FormColor; } export default function FormSettingsPage() { - const router = useRouter(); + const router = useRouter(); - const host = useHost(); + const host = useHost(); - const { - register, - formState, - handleSubmit, - watch, - control, - setValue, - } = useForm(); - const { errors } = formState; + const { + register, + formState, + handleSubmit, + watch, + control, + setValue, + } = useForm(); + const { errors } = formState; - const showModal = useModal(); + const showModal = useModal(); - const { tab, formSlug } = router.query; + const { tab, formSlug } = router.query; - const [forms, loading, error] = useCollectionData( - formSlug - ? firestore.collection("forms").where("slug", "==", formSlug) - : null - ); + const [forms, loading, error] = useCollectionData( + formSlug + ? firestore.collection("forms").where("slug", "==", formSlug) + : null + ); - const showSnackbar = useSnack(); + const showSnackbar = useSnack(); - useEffect(() => { - if (forms) { - setValue("name", forms[0].name); - setValue("description", forms[0].description); - setValue("color", forms[0].color); - } - }, [forms, setValue]); + useEffect(() => { + if (forms) { + setValue("name", forms[0].name); + setValue("description", forms[0].description); + setValue("color", forms[0].color); + } + }, [forms, setValue]); - async function onSubmit(data: CreateFormForm) { - const { color, description, name } = data; + async function onSubmit(data: CreateFormForm) { + const { color, description, name } = data; - const { - docs: [formToChange], - } = await firestore.collection("forms").where("slug", "==", formSlug).get(); + const { + docs: [formToChange], + } = await firestore.collection("forms").where("slug", "==", formSlug).get(); - await formToChange.ref.update({ - color, - name, - description, - }); + await formToChange.ref.update({ + color, + name, + description, + }); - showSnackbar("Form information updated!"); - } + showSnackbar("Form information updated!"); + } - const [switchValue, setSwitchValue] = useState(false); + const [switchValue, setSwitchValue] = useState(false); - async function changeSubmissionStatus(newValue: boolean) { - setSwitchValue(newValue); + async function changeSubmissionStatus(newValue: boolean) { + setSwitchValue(newValue); - const { - docs: [formToChange], - } = await firestore.collection("forms").where("slug", "==", formSlug).get(); + const { + docs: [formToChange], + } = await firestore.collection("forms").where("slug", "==", formSlug).get(); - await formToChange.ref.update({ - allowSubmissions: newValue, - }); - } + await formToChange.ref.update({ + allowSubmissions: newValue, + }); + } - let tabView: ReactNode; + let tabView: ReactNode; - switch (tab) { - case "submit": - tabView = ( -
-
- - - Enable submissions - - - When disabled, any submissions will be denied. - - - -
-
-
-

How submission works

-

- Submitting form data is as easy as sending an HTTP request to the - submission api, which is deployed right along this dashboard. -

+ switch (tab) { + case "submit": + tabView = ( +
+
+ + + Enable submissions + + + When disabled, any submissions will be denied. + + + +
+
+
+

How submission works

+

+ Submitting form data is as easy as sending an HTTP request to the + submission api, which is deployed right along this dashboard. +

-

- You send a POST-request to{" "} - https://{host}/api/submit with the ID of this form as - the `formId` URL parameter. Send any data as JSON in the request - body. Do not forget to specify the Content-Type{" "} - header as application/json. -

-

- Here is a reference implementation of how to submit an entry for - this form in JavaScript, where the data argument is - an arbitrary JSON object: -

- {`async function handleFormSubmission(data) { +

+ You send a POST-request to{" "} + https://{host}/api/submit with the ID of this form as + the `formId` URL parameter. Send any data as JSON in the request + body. Do not forget to specify the Content-Type{" "} + header as application/json. +

+

+ Here is a reference implementation of how to submit an entry for + this form in JavaScript, where the data argument is + an arbitrary JSON object: +

+ {`async function handleFormSubmission(data) { const response = await fetch("https://${host}/api/forms/${forms?.[0]?.id}/submissions", { method: "POST", body: JSON.stringify(data), @@ -153,247 +153,247 @@ export default function FormSettingsPage() { throw Error(\`Submission failed with status code \${response.status}\`) } }`} -
-
- ); - break; - case "danger": - tabView = ( -
-
- - - Delete this form - - - Once a form is deleted, itself and all its submissions will be - deleted. Please be certain. - - -
+
+ ); + break; + case "danger": + tabView = ( +
+
+ + + Delete this form + + + Once a form is deleted, itself and all its submissions will be + deleted. Please be certain. + + + -
-
- ); - break; - default: - tabView = ( -
-
-
-

- General Information -

-

- This information is only used for this interface and is not - exposed to the public. -

-
- -
-
-
-
- -
-
- - - {`${host}/${formSlug}`} - -
- {errors.name && ( -

- {errors.name.message} -

- )} -

- The slug (used in URLs) cannot be changed. -

-
-
-
- -
-