├── .DS_Store ├── .gitignore ├── README.md ├── backend ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .vscode │ └── typescript.code-snippets ├── jest.config.js ├── package.json ├── readme.md ├── src │ ├── middleware │ │ ├── checkers.ts │ │ ├── index.ts │ │ └── verifytoken.ts │ ├── models │ │ ├── Item │ │ │ ├── index.ts │ │ │ ├── item-examples.ts │ │ │ ├── item-model.ts │ │ │ ├── item-types.ts │ │ │ └── item.test.ts │ │ ├── Registry │ │ │ ├── index.ts │ │ │ ├── registry-examples.ts │ │ │ ├── registry-model.ts │ │ │ ├── registry-types.ts │ │ │ └── registry.test.ts │ │ └── index.ts │ ├── routes │ │ ├── Items │ │ │ ├── index.ts │ │ │ ├── items-controllers.ts │ │ │ ├── items-routes.ts │ │ │ └── items.test.ts │ │ ├── Registry │ │ │ ├── index.ts │ │ │ ├── registry-controllers.ts │ │ │ ├── registry-routes.ts │ │ │ └── registry.test.ts │ │ └── index.ts │ ├── server │ │ ├── app.test.ts │ │ ├── app.ts │ │ ├── database.ts │ │ └── index.ts │ ├── test │ │ ├── index.ts │ │ ├── mockcontrollers │ │ │ ├── index.ts │ │ │ ├── mock-items.ts │ │ │ └── mock-registry.ts │ │ ├── request.ts │ │ └── setup │ │ │ ├── authsetup.ts │ │ │ ├── dbsetup.ts │ │ │ └── index.ts │ └── utils │ │ ├── authhandler.ts │ │ ├── index.ts │ │ ├── item.ts │ │ ├── permissions.ts │ │ ├── pricing.ts │ │ └── tokens.ts ├── tsconfig.json └── yarn.lock └── frontend ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── components ├── AdminItemsTable.js ├── AdminPage │ ├── AdminPage.js │ ├── Nav │ │ ├── Nav.js │ │ ├── NavMenu.js │ │ ├── NavMenuLink.js │ │ └── index.js │ ├── SideBar │ │ ├── SideBar.js │ │ ├── SideBarLink.js │ │ ├── SideBarLinks.js │ │ └── index.js │ └── index.js ├── Button.js ├── DocsAPI │ ├── DocsAPI.js │ ├── Endpoints.js │ ├── TextContainer.js │ ├── data.js │ ├── index.js │ └── utils.js ├── Footer.js ├── Header.js ├── InputText.js ├── Item.js ├── ItemForm.js ├── Items.js ├── LandingPage │ ├── Hero.js │ ├── Info.js │ ├── Landing.js │ ├── index.js │ └── svgs │ │ ├── GiftBoxSVG.js │ │ ├── GiftCardSVG.js │ │ ├── GiftSVG.js │ │ ├── GiftSendSVG.js │ │ ├── GiftWishesSVG.js │ │ └── index.js ├── Link.js ├── Loader.js ├── Modal.js ├── NavBar.js ├── Portal.js ├── PurchaseItem.js ├── Purchases.js ├── RegistryForm.js └── Snack.js ├── css ├── colors.js └── tailwind.css ├── next.config.js ├── package.json ├── pages ├── [registryUrl].js ├── _app.js ├── _document.js ├── admin.js ├── admin │ ├── gifts.js │ └── gifts │ │ ├── [gift_id].js │ │ ├── create.js │ │ └── purchases.js ├── create.js ├── docs │ └── api.js ├── index.js └── welcome.js ├── postcss.config.js ├── public ├── favicon.ico └── images │ ├── chingu.png │ ├── default_gift_image-10.jpg │ ├── default_gift_image.jpg │ ├── default_profile_image.jpg │ └── hero.jpg ├── tailwind.config.js ├── types └── index.js ├── utils ├── config.js ├── fetch.js ├── index.js └── prices.js └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamesWangDev/BearApp-React-/73743a4bed69cf8ce592326403ab4452659f82c1/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .now 2 | frontend/.vscode/* 3 | frontend/.eslintcache 4 | backend/.vscode/* 5 | !backend/.vscode/typescript.code-snippets 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier/@typescript-eslint", 6 | "plugin:prettier/recommended" 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "semi": ["error", "always"], 14 | "quotes": ["error", "double"], 15 | "@typescript-eslint/explicit-function-return-type": "off", 16 | "@typescript-eslint/no-explicit-any": 1, 17 | "@typescript-eslint/no-inferrable-types": [ 18 | "warn", 19 | { 20 | "ignoreParameters": true 21 | } 22 | ], 23 | // if you have a parameter that isn't used add an underscore ... 24 | // ... at the front to ignore it, i.e. (_req, res, next) ... 25 | // ... consequently, ESLint will ignore the _req param above 26 | "@typescript-eslint/no-unused-vars": [ 27 | "error", 28 | { 29 | "vars": "all", 30 | "args": "all", 31 | "ignoreRestSiblings": false, 32 | "argsIgnorePattern": "^_" 33 | } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | .env -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "printWidth": 80, 5 | "semi": true, 6 | "singleQuote": false, 7 | "tabWidth": 2, 8 | "trailingComma": "es5" 9 | } 10 | -------------------------------------------------------------------------------- /backend/.vscode/typescript.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "newcontrollers": { 3 | "scope": "typescript", 4 | "prefix": "newcontrollers", 5 | "body": [ 6 | "import { RequestHandler } from 'express';", 7 | "", 8 | "${1:controller}" 9 | ], 10 | "description": "starts a new controller file. Ctrl+space to make your first controller" 11 | }, 12 | 13 | "controller": { 14 | "scope": "typescript", 15 | "prefix": "controller", 16 | "body": [ 17 | "export const $1: RequestHandler = async (req, res, next) => {", 18 | " try {", 19 | " $0", 20 | " } catch(err){", 21 | " next(err);", 22 | " }", 23 | "}" 24 | ], 25 | "description": "creates a route controller function" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | }; 5 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/server/index.ts", 6 | "scripts": { 7 | "dev": "cross-env NODE_ENV=development nodemon -r dotenv/config", 8 | "start": "node -r dotenv/config dist/server/index.js", 9 | "test": "jest --setupFiles dotenv/config --runInBand", 10 | "test:watch": "yarn test --watch", 11 | "postinstall": "tsc" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "engines": { 17 | "yarn": "1.22.0" 18 | }, 19 | "dependencies": { 20 | "@types/cors": "^2.8.6", 21 | "@types/dotenv": "^8.2.0", 22 | "@types/eslint": "^6.1.8", 23 | "@types/express": "^4.17.2", 24 | "@types/express-jwt": "^0.0.42", 25 | "@types/http-errors": "^1.6.3", 26 | "@types/jest": "^25.1.3", 27 | "@types/mongoose": "^5.7.1", 28 | "@types/node": "^13.7.1", 29 | "@types/supertest": "^2.0.8", 30 | "cors": "^2.8.5", 31 | "dotenv": "^8.2.0", 32 | "express": "^4.17.1", 33 | "express-jwt": "^5.3.1", 34 | "http-errors": "^1.7.3", 35 | "jwks-rsa": "^1.7.0", 36 | "mongoose": "^5.8.11", 37 | "morgan-body": "^2.4.8" 38 | }, 39 | "devDependencies": { 40 | "@shelf/jest-mongodb": "^1.1.3", 41 | "@typescript-eslint/eslint-plugin": "^2.19.2", 42 | "@typescript-eslint/parser": "^2.19.2", 43 | "cross-env": "^7.0.0", 44 | "eslint": "^6.8.0", 45 | "eslint-config-prettier": "^6.10.0", 46 | "eslint-plugin-prettier": "^3.1.2", 47 | "jest": "^25.1.0", 48 | "mock-jwks": "^0.3.0", 49 | "mongodb-memory-server": "^6.2.4", 50 | "nock": "^12.0.2", 51 | "nodemon": "^2.0.2", 52 | "prettier": "^1.19.1", 53 | "supertest": "^4.0.2", 54 | "ts-jest": "^25.2.1", 55 | "ts-node": "^8.6.2", 56 | "typescript": "^3.7.5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /backend/readme.md: -------------------------------------------------------------------------------- 1 | # Wedding Registry Backend 2 | 3 | **Express** API connected to **MongoDB** through **Mongoose**, written in **TypeScript**. 4 | 5 | Contained over 40 **Jest** tests, testing things like the app, models and routes. 6 | 7 | Uses **ESLint** and **Prettier** to help keep all code consistent. 8 | 9 | **Setup:** 10 | 11 | - install `npm i -g yarn` 12 | - navigate to backend root and run `yarn` 13 | - create a `.env` file like below 14 | - run `yarn dev` to start the server 15 | - open http://localhost:3000/api 16 | - run `yarn test` to see the test coverage 17 | 18 | ``` 19 | DB_USER = dbUserHere 20 | DB_PASSWORD = dbPasswordHere 21 | DB_URL = dbHere *refer to notes below* 22 | 23 | AUTH0_DOMAIN = Auth0DomainHere 24 | AUTH0_CLIENT_ID = Auth0ClientIdHere 25 | AUTH0_API_IDENTIFIER = Auth0APIRouteHere 26 | 27 | PERMISSIONS_ADMIN = arrayOfAdminPermissionsHere 28 | PERMISSIONS_PAIDUSER = arrayOfPaidUserPermissionsHere 29 | 30 | ``` 31 | 32 | **Notes:** 33 | 34 | - make sure you have prettier and eslint extensions installed on your IDE 35 | - don't specify the database name, because the app will create production and development collections for you automatically 36 | - ex) mongodb.com DB urls usually have `/test` at the end. You'll just want the `/` 37 | -------------------------------------------------------------------------------- /backend/src/middleware/checkers.ts: -------------------------------------------------------------------------------- 1 | import createError from "http-errors"; 2 | import { Registry } from "../models"; 3 | import { 4 | AuthHandler, 5 | checkPermissions, 6 | permissionsAdmin, 7 | permissionsPaidUser, 8 | } from "../utils"; 9 | 10 | // checks if the verified user has the paid user permissions 11 | export const checkPaidUser: AuthHandler = async (req, _res, next) => { 12 | try { 13 | checkPermissions(req.user?.permissions, permissionsPaidUser); 14 | next(); 15 | } catch (err) { 16 | next(err); 17 | } 18 | }; 19 | 20 | // checks if the verified user has the admin permissions 21 | export const checkAdmin: AuthHandler = async (req, _res, next) => { 22 | try { 23 | checkPermissions(req.user?.permissions, permissionsAdmin); 24 | next(); 25 | } catch (err) { 26 | next(err); 27 | } 28 | }; 29 | 30 | // compare req.user.sub and registry.userId 31 | // any routes that uses this NEEDS a registryId param 32 | export const checkOwnership: AuthHandler = async (req, _res, next) => { 33 | try { 34 | const { registryId } = req.params; 35 | 36 | // find the registry 37 | const registry = await Registry.findById(registryId); 38 | if (!registry) throw createError(404, `Registry (${registryId}) not found`); 39 | 40 | // check if user is the owner 41 | if (registry.userId === req.user?.sub) return next(); 42 | 43 | // check if the user is an admin 44 | checkPermissions(req.user?.permissions, permissionsAdmin, "ownership"); 45 | 46 | next(); 47 | } catch (err) { 48 | next(err); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /backend/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { verifyToken } from "./verifytoken"; 2 | import { checkPaidUser, checkAdmin, checkOwnership } from "./checkers"; 3 | 4 | export { verifyToken, checkPaidUser, checkAdmin, checkOwnership }; 5 | -------------------------------------------------------------------------------- /backend/src/middleware/verifytoken.ts: -------------------------------------------------------------------------------- 1 | import jwt from "express-jwt"; 2 | import jwksRsa from "jwks-rsa"; 3 | 4 | // ENVIRONMENT VARIABLES USED 5 | const NODE_ENV = process.env.NODE_ENV; 6 | const AUTH0_API_IDENTIFIER = process.env.AUTH0_API_IDENTIFIER; 7 | const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN; 8 | const isTest = NODE_ENV === "test"; 9 | 10 | // CREATE JWT OPTIONS 11 | const { cache, audience, issuer, jwksUri, jwksRequestsPerMinute } = { 12 | cache: !isTest, 13 | audience: isTest ? "private" : AUTH0_API_IDENTIFIER, 14 | issuer: isTest ? "master" : `https://${AUTH0_DOMAIN}/`, 15 | jwksUri: `https://${AUTH0_DOMAIN}/.well-known/jwks.json`, 16 | jwksRequestsPerMinute: isTest ? 100 : 1000, 17 | }; 18 | 19 | // verifies there is a token and that it was verified successfully 20 | export const verifyToken = jwt({ 21 | secret: jwksRsa.expressJwtSecret({ 22 | cache, 23 | rateLimit: true, 24 | jwksRequestsPerMinute, 25 | jwksUri, 26 | }), 27 | 28 | audience, 29 | issuer, 30 | algorithm: ["RS256"], 31 | }); 32 | -------------------------------------------------------------------------------- /backend/src/models/Item/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./item-model"; 2 | export { ItemI } from "./item-types"; 3 | export { 4 | validItem, 5 | invalidItem, 6 | invalidName, 7 | invalidDescription, 8 | invalidPrice, 9 | } from "./item-examples"; 10 | -------------------------------------------------------------------------------- /backend/src/models/Item/item-examples.ts: -------------------------------------------------------------------------------- 1 | export const validItem = { 2 | name: "Honda Civic", 3 | description: "We want a new car!", 4 | price: 20000, 5 | }; 6 | 7 | export const invalidItem = { 8 | name: "Honda Civic", 9 | price: -1, 10 | }; 11 | 12 | export const invalidName = { 13 | description: "We want a new car!", 14 | price: 20000, 15 | }; 16 | 17 | export const invalidDescription = { 18 | name: "Honda Civic", 19 | price: 20000, 20 | }; 21 | 22 | export const invalidPrice = { 23 | name: "Honda Civic", 24 | description: "We want a new car!", 25 | }; 26 | -------------------------------------------------------------------------------- /backend/src/models/Item/item-model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { ItemI } from "./item-types"; 3 | 4 | const ItemSchema = new mongoose.Schema({ 5 | name: { 6 | type: String, 7 | required: [true, "Item name required"], 8 | }, 9 | description: { 10 | type: String, 11 | required: [true, "Item description required"], 12 | }, 13 | price: { 14 | type: Number, 15 | required: [true, "Item price required"], 16 | min: 0, 17 | }, 18 | link: { 19 | type: String, 20 | }, 21 | image: { 22 | type: String, 23 | }, 24 | isReserved: { 25 | type: Boolean, 26 | default: false, 27 | }, 28 | addedOn: { 29 | type: Date, 30 | default: Date.now, 31 | }, 32 | reservedOn: { 33 | type: Date, 34 | }, 35 | purchasers: [ 36 | { 37 | name: String, 38 | email: String, 39 | message: String, 40 | purchasedOn: { 41 | type: Date, 42 | default: Date.now, 43 | }, 44 | pricePaid: { 45 | type: Number, 46 | required: [true, "Item pricePaid required"], 47 | min: 0, 48 | }, 49 | }, 50 | ], 51 | }); 52 | 53 | const Item = mongoose.model("Item", ItemSchema); 54 | 55 | export default Item; 56 | -------------------------------------------------------------------------------- /backend/src/models/Item/item-types.ts: -------------------------------------------------------------------------------- 1 | import { Document } from "mongoose"; 2 | 3 | export interface PurchaserI { 4 | name?: string; 5 | email?: string; 6 | message?: string; 7 | purchasedOn: Date; 8 | pricePaid: number; 9 | } 10 | export interface ItemI extends Document { 11 | _id: string; 12 | name: string; 13 | description: string; 14 | price: number; 15 | link?: string; 16 | image?: string; 17 | isReserved?: boolean; 18 | addedOn?: Date; 19 | reservedOn?: Date; 20 | purchasers?: PurchaserI[]; 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/models/Item/item.test.ts: -------------------------------------------------------------------------------- 1 | import Item from "./item-model"; 2 | import { 3 | validItem, 4 | invalidItem, 5 | invalidName, 6 | invalidDescription, 7 | invalidPrice, 8 | } from "./item-examples"; 9 | 10 | // used to check model validation rules 11 | async function itemMocker(newItem: {}) { 12 | let error = ""; 13 | try { 14 | const item = new Item(newItem); 15 | await item.validate(); 16 | } catch (err) { 17 | error = err.message; 18 | } 19 | return error; 20 | } 21 | 22 | const errorMsg = (field: string) => { 23 | return `Item validation failed: ${field}: Item ${field} required`; 24 | }; 25 | 26 | describe("Item Model Tests", () => { 27 | test("expected successful item validation", async () => { 28 | const error = await itemMocker(validItem); 29 | expect(error).toBeFalsy(); 30 | }); 31 | 32 | test("expected unsuccessful item validation", async () => { 33 | const error = await itemMocker(invalidItem); 34 | expect(error).toBeTruthy(); 35 | }); 36 | 37 | test("expected unsuccessful item validation - name", async () => { 38 | const error = await itemMocker(invalidName); 39 | expect(error).toBe(errorMsg("name")); 40 | }); 41 | 42 | test("expected unsuccessful item validation - description", async () => { 43 | const error = await itemMocker(invalidDescription); 44 | expect(error).toBe(errorMsg("description")); 45 | }); 46 | 47 | test("expected unsuccessful item validation - price", async () => { 48 | const error = await itemMocker(invalidPrice); 49 | expect(error).toBe(errorMsg("price")); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /backend/src/models/Registry/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./registry-model"; 2 | export { 3 | validRegistry, 4 | invalidRegistry, 5 | invalidTitle, 6 | invalidDescription, 7 | invalidP1FullName, 8 | invalidP2FullName, 9 | invalidUserId, 10 | invalidEmail, 11 | invalidCustomUrl, 12 | } from "./registry-examples"; 13 | -------------------------------------------------------------------------------- /backend/src/models/Registry/registry-examples.ts: -------------------------------------------------------------------------------- 1 | export const validRegistry = { 2 | title: "Rose and Mel's Wedding", 3 | description: "Come celebrate", 4 | userId: "1234567890", 5 | customUrl: "RoseAndMel", 6 | weddingDate: "2020-02-26T09:42:50.667Z", 7 | tyMessage: "Thanks for the Gift", 8 | p1FullName: "Rose Maf", 9 | p2FullName: "Mel Ras", 10 | phoneNumber: 3217890, 11 | email: "rose@gmail.com", 12 | coverImage: "https://bit.ly/2Pr0xeQ", 13 | }; 14 | 15 | export const invalidRegistry = { 16 | //title: "Rose and Mel's Wedding", 17 | // description: "Come celebrate", 18 | userId: "1234567890", 19 | customUrl: "RoseAndMel", 20 | weddingDate: "2020-02-26T09:42:50.667Z", 21 | tyMessage: "Thanks for the Gift", 22 | // p1FullName: "Rose Maf", 23 | p2FullName: "Mel Ras", 24 | phoneNumber: 3217890, 25 | email: "rose@gmail.com", 26 | coverImage: "https://bit.ly/2Pr0xeQ", 27 | }; 28 | 29 | export const invalidTitle = { 30 | //title: "Rose and Mel's Wedding", 31 | description: "Come celebrate", 32 | userId: "1234567890", 33 | customUrl: "RoseAndMel", 34 | weddingDate: "2020-02-26T09:42:50.667Z", 35 | tyMessage: "Thanks for the Gift", 36 | p1FullName: "Rose Maf", 37 | p2FullName: "Mel Ras", 38 | phoneNumber: 3217890, 39 | email: "rose@gmail.com", 40 | coverImage: "https://bit.ly/2Pr0xeQ", 41 | }; 42 | 43 | export const invalidDescription = { 44 | title: "Rose and Mel's Wedding", 45 | // description: "Come celebrate", 46 | userId: "1234567890", 47 | customUrl: "RoseAndMel", 48 | weddingDate: "2020-02-26T09:42:50.667Z", 49 | tyMessage: "Thanks for the Gift", 50 | p1FullName: "Rose Maf", 51 | p2FullName: "Mel Ras", 52 | phoneNumber: 3217890, 53 | email: "rose@gmail.com", 54 | coverImage: "https://bit.ly/2Pr0xeQ", 55 | }; 56 | 57 | export const invalidP1FullName = { 58 | title: "Rose and Mel's Wedding", 59 | description: "Come celebrate", 60 | userId: "1234567890", 61 | customUrl: "RoseAndMel", 62 | weddingDate: "2020-02-26T09:42:50.667Z", 63 | tyMessage: "Thanks for the Gift", 64 | // p1FullName: "Rose Maf", 65 | p2FullName: "Mel Ras", 66 | phoneNumber: 3217890, 67 | email: "rose@gmail.com", 68 | coverImage: "https://bit.ly/2Pr0xeQ", 69 | }; 70 | 71 | export const invalidP2FullName = { 72 | title: "Rose and Mel's Wedding", 73 | description: "Come celebrate", 74 | userId: "1234567890", 75 | customUrl: "RoseAndMel", 76 | weddingDate: "2020-02-26T09:42:50.667Z", 77 | tyMessage: "Thanks for the Gift", 78 | p1FullName: "Rose Maf", 79 | // p2FullName: "Mel Ras", 80 | phoneNumber: 3217890, 81 | email: "rose@gmail.com", 82 | coverImage: "https://bit.ly/2Pr0xeQ", 83 | }; 84 | 85 | export const invalidEmail = { 86 | title: "Rose and Mel's Wedding", 87 | description: "Come celebrate", 88 | userId: "1234567890", 89 | customUrl: "RoseAndMel", 90 | weddingDate: "2020-02-26T09:42:50.667Z", 91 | tyMessage: "Thanks for the Gift", 92 | p1FullName: "Rose Maf", 93 | p2FullName: "Mel Ras", 94 | phoneNumber: 3217890, 95 | // email: "rose@gmail.com", 96 | coverImage: "https://bit.ly/2Pr0xeQ", 97 | }; 98 | 99 | export const invalidUserId = { 100 | title: "Rose and Mel's Wedding", 101 | description: "Come celebrate", 102 | // userId: "1234567890", 103 | customUrl: "RoseAndMel", 104 | weddingDate: "2020-02-26T09:42:50.667Z", 105 | tyMessage: "Thanks for the Gift", 106 | p1FullName: "Rose Maf", 107 | p2FullName: "Mel Ras", 108 | phoneNumber: 3217890, 109 | email: "rose@gmail.com", 110 | coverImage: "https://bit.ly/2Pr0xeQ", 111 | }; 112 | 113 | export const invalidCustomUrl = { 114 | title: "Rose and Mel's Wedding", 115 | description: "Come celebrate", 116 | userId: "1234567890", 117 | // customUrl: "RoseAndMel", 118 | weddingDate: "2020-02-26T09:42:50.667Z", 119 | tyMessage: "Thanks for the Gift", 120 | p1FullName: "Rose Maf", 121 | p2FullName: "Mel Ras", 122 | phoneNumber: 3217890, 123 | email: "rose@gmail.com", 124 | coverImage: "https://bit.ly/2Pr0xeQ", 125 | }; 126 | -------------------------------------------------------------------------------- /backend/src/models/Registry/registry-model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { RegistryI } from "./registry-types"; 3 | 4 | const RegistrySchema = new mongoose.Schema({ 5 | title: { 6 | type: String, 7 | required: [true, "Registry title required"], 8 | }, 9 | description: { 10 | type: String, 11 | required: [true, "Registry description required"], 12 | }, 13 | tyMessage: { 14 | type: String, 15 | }, 16 | p1FullName: { 17 | type: String, 18 | required: [true, "Registry p1FullName required"], 19 | }, 20 | p2FullName: { 21 | type: String, 22 | required: [true, "Registry p2FullName required"], 23 | }, 24 | weddingDate: { 25 | type: Date, 26 | }, 27 | phoneNumber: { 28 | type: Number, 29 | }, 30 | email: { 31 | type: String, 32 | required: [true, "Registry email required"], 33 | }, 34 | userId: { 35 | type: String, 36 | required: [true, "Registry userId required"], 37 | }, 38 | customUrl: { 39 | type: String, 40 | required: [true, "Registry customUrl required"], 41 | unique: true, 42 | validate: { 43 | validator: (url: string) => url.split(" ").length === 1, 44 | message: () => "Registry customUrl cannot contain spaces", 45 | }, 46 | }, 47 | items: [ 48 | { 49 | type: mongoose.Types.ObjectId, 50 | ref: "Item", 51 | }, 52 | ], 53 | coverImage: { 54 | type: String, 55 | default: "https://bit.ly/2Pr0xeQ", 56 | }, 57 | }); 58 | 59 | const Registry = mongoose.model("Registry", RegistrySchema); 60 | 61 | export default Registry; 62 | -------------------------------------------------------------------------------- /backend/src/models/Registry/registry-types.ts: -------------------------------------------------------------------------------- 1 | import { Document } from "mongoose"; 2 | import { ItemI } from "../Item"; 3 | 4 | export interface RegistryI extends Document { 5 | title: string; 6 | description: string; 7 | tyMessage?: string; 8 | p1FullName: string; 9 | p2FullName: string; 10 | weddingDate?: Date; 11 | phoneNumber?: number; 12 | email: string; 13 | userId: string; 14 | customUrl: string; 15 | items: (string | ItemI)[]; 16 | coverImage: string; 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/models/Registry/registry.test.ts: -------------------------------------------------------------------------------- 1 | import Registry from "./registry-model"; 2 | import { 3 | validRegistry, 4 | invalidRegistry, 5 | invalidTitle, 6 | invalidDescription, 7 | invalidP1FullName, 8 | invalidP2FullName, 9 | invalidUserId, 10 | invalidEmail, 11 | invalidCustomUrl, 12 | } from "./registry-examples"; 13 | 14 | async function registryMocker(newRegistry: {}) { 15 | let error = ""; 16 | try { 17 | const registry = new Registry(newRegistry); 18 | await registry.validate(); 19 | } catch (err) { 20 | error = err.message; 21 | } 22 | return error; 23 | } 24 | 25 | const errorMsg = (field: string) => { 26 | return `Registry validation failed: ${field}: Registry ${field} required`; 27 | }; 28 | 29 | describe("Registry Model Tests", () => { 30 | test("Expected successful registry validation", async () => { 31 | const error = await registryMocker(validRegistry); 32 | expect(error).toBeFalsy(); 33 | }); 34 | 35 | test("expected unsuccessful registry validation", async () => { 36 | const error = await registryMocker(invalidRegistry); 37 | expect(error).toBeTruthy(); 38 | }); 39 | 40 | test("expected unsuccessful registry validation - title", async () => { 41 | const error = await registryMocker(invalidTitle); 42 | expect(error).toBe(errorMsg("title")); 43 | }); 44 | 45 | test("expected unsuccessful registry validation - description", async () => { 46 | const error = await registryMocker(invalidDescription); 47 | expect(error).toBe(errorMsg("description")); 48 | }); 49 | 50 | test("expected unsuccessful registry validation - p1FullName", async () => { 51 | const error = await registryMocker(invalidP1FullName); 52 | expect(error).toBe(errorMsg("p1FullName")); 53 | }); 54 | 55 | test("expected unsuccessful registry validation - p2FullName", async () => { 56 | const error = await registryMocker(invalidP2FullName); 57 | expect(error).toBe(errorMsg("p2FullName")); 58 | }); 59 | 60 | test("expected unsuccessful registry validation - userId", async () => { 61 | const error = await registryMocker(invalidUserId); 62 | expect(error).toBe(errorMsg("userId")); 63 | }); 64 | 65 | test("expected unsuccessful registry validation - email", async () => { 66 | const error = await registryMocker(invalidEmail); 67 | expect(error).toBe(errorMsg("email")); 68 | }); 69 | 70 | test("expected unsuccessful registry validation - customUrl", async () => { 71 | const error = await registryMocker(invalidCustomUrl); 72 | expect(error).toBe(errorMsg("customUrl")); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /backend/src/models/index.ts: -------------------------------------------------------------------------------- 1 | import Item from "./Item"; 2 | import Registry from "./Registry"; 3 | 4 | export { Item, Registry }; 5 | -------------------------------------------------------------------------------- /backend/src/routes/Items/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./items-routes"; 2 | -------------------------------------------------------------------------------- /backend/src/routes/Items/items-controllers.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import mongoose from "mongoose"; 3 | import createError from "http-errors"; 4 | import { Item, Registry } from "../../models"; 5 | import { structureItem } from "../../utils"; 6 | 7 | export const getOneItem: RequestHandler = async (req, res, next) => { 8 | try { 9 | const { itemId } = req.params; 10 | const item = await Item.findById(itemId).lean(); 11 | if (!item) throw createError(404, `Item (${itemId}) not found`); 12 | res.status(200).json(item); 13 | } catch (err) { 14 | next(err); 15 | } 16 | }; 17 | 18 | export const getEveryItem: RequestHandler = async (_req, res, next) => { 19 | try { 20 | const items = await Item.find().lean(); 21 | res.status(200).json(items); 22 | } catch (err) { 23 | next(err); 24 | } 25 | }; 26 | 27 | export const createItem: RequestHandler = async (req, res, next) => { 28 | try { 29 | const { registryId } = req.params; 30 | 31 | // find the registry 32 | const registry = await Registry.findById(registryId); 33 | if (!registry) throw createError(404, `Registry (${registryId}) not found`); 34 | 35 | // create an item 36 | const newItem = await Item.create(req.body); 37 | 38 | // push the newItem _id into the registry items array 39 | registry.items.push(newItem._id); 40 | await registry.save(); 41 | 42 | res.status(201).json(newItem); 43 | } catch (err) { 44 | next(err); 45 | } 46 | }; 47 | 48 | export const deleteOneItem: RequestHandler = async (req, res, next) => { 49 | try { 50 | const { itemId, registryId } = req.params; 51 | 52 | const deletedItem = await Item.findByIdAndDelete(itemId); 53 | if (!deletedItem) { 54 | throw createError(400, `Error removing item (${itemId})`); 55 | } 56 | 57 | const updatedRegistry = await Registry.findByIdAndUpdate(registryId, { 58 | $pull: { items: itemId }, 59 | }); 60 | if (!updatedRegistry) { 61 | throw createError(400, `Error updating Registry (${registryId}) items`); 62 | } 63 | 64 | res.status(200).json(deletedItem); 65 | } catch (err) { 66 | next(err); 67 | } 68 | }; 69 | 70 | export const updateOneItem: RequestHandler = async (req, res, next) => { 71 | try { 72 | const { itemId } = req.params; 73 | 74 | const updatedItem = await Item.findByIdAndUpdate(itemId, req.body, { 75 | new: true, 76 | runValidators: true, 77 | }); 78 | if (!updatedItem) throw createError(400, `Error updating item (${itemId})`); 79 | 80 | res.status(200).json(updatedItem); 81 | } catch (err) { 82 | next(err); 83 | } 84 | }; 85 | 86 | export const getItemsByRegistry: RequestHandler = async (req, res, next) => { 87 | try { 88 | const { registryId } = req.params; 89 | console.log(registryId); 90 | 91 | // find the target registry 92 | const registry = await Registry.findById(registryId); 93 | if (!registry) throw createError(404, `Registry (${registryId}) not found`); 94 | 95 | const itemIds = registry.items; 96 | 97 | // show all the items passed in array Of Items 98 | const items = await Item.find({ 99 | _id: { 100 | $in: itemIds, 101 | }, 102 | }); 103 | 104 | res.status(200).json(items); 105 | } catch (err) { 106 | next(err); 107 | } 108 | }; 109 | 110 | export const deleteMultipleItems: RequestHandler = async (req, res, next) => { 111 | try { 112 | const { registryId } = req.params; 113 | const { arrayOfIds } = req.body; 114 | 115 | // throws if you forgot to pass arrayOfIds 116 | if (!arrayOfIds) { 117 | throw createError(400, "Please pass arrayOfIds in the body"); 118 | } 119 | 120 | // change array of string into mongoose ObjectId's 121 | const idsToDelete = JSON.parse(arrayOfIds).map((id: string) => 122 | mongoose.Types.ObjectId(id) 123 | ); 124 | 125 | // throws if you passed an empty arrayOfIds 126 | if (!idsToDelete.length) { 127 | throw createError(400, "You didn't put any _id's in arrayOfIds"); 128 | } 129 | 130 | // find the target registry 131 | const registry = await Registry.findById(registryId); 132 | if (!registry) throw createError(404, `Registry (${registryId}) not found`); 133 | 134 | // delete all the items passed in arrayOfIds 135 | const { deletedCount, n } = await Item.deleteMany({ 136 | _id: { 137 | $in: idsToDelete, 138 | }, 139 | }); 140 | 141 | // throws if we didn't even attempt to delete anything 142 | if (!n) { 143 | throw createError(400, "Sorry, couldn't find those _ids"); 144 | } 145 | 146 | // throws if we didn't delete anything 147 | if (!deletedCount) { 148 | throw createError(400, `Sorry, deleted 0/${n} items`); 149 | } 150 | 151 | // remove those items from their registry 152 | const updatedRegistry = await Registry.findByIdAndUpdate( 153 | registryId, 154 | { 155 | $pull: { 156 | items: { 157 | $in: idsToDelete, 158 | }, 159 | }, 160 | }, 161 | { new: true, runValidators: true } 162 | ); 163 | if (!updatedRegistry) { 164 | throw createError(400, `Error updating registry (${registryId})`); 165 | } 166 | 167 | const message = `Updated registry items and deleted ${deletedCount}/${n} item${ 168 | n === 1 ? "" : "s" 169 | }`; 170 | 171 | res.status(200).json({ message }); 172 | } catch (err) { 173 | next(err); 174 | } 175 | }; 176 | 177 | export const madeItemPurchase: RequestHandler = async (req, res, next) => { 178 | try { 179 | const { itemId } = req.params; 180 | 181 | if (!req.body.pricePaid) { 182 | throw createError(400, "You forgot to pass in the pricePaid"); 183 | } 184 | 185 | const item = await Item.findById(itemId); 186 | if (!item) throw createError(404, `Item (${itemId}) not found`); 187 | 188 | if (item.purchasers) { 189 | item.purchasers.push(req.body); 190 | } else { 191 | item.purchasers = [req.body]; 192 | } 193 | 194 | await item.save(); 195 | 196 | res.status(200).json(structureItem(item)); 197 | } catch (err) { 198 | next(err); 199 | } 200 | }; 201 | -------------------------------------------------------------------------------- /backend/src/routes/Items/items-routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { verifyToken, checkOwnership } from "../../middleware"; 3 | import { 4 | getEveryItem, 5 | getOneItem, 6 | updateOneItem, 7 | deleteOneItem, 8 | createItem, 9 | deleteMultipleItems, 10 | madeItemPurchase, 11 | getItemsByRegistry, 12 | } from "./items-controllers"; 13 | 14 | const router = Router(); 15 | 16 | router.get("/all", getEveryItem); 17 | router 18 | .route("/:itemId") 19 | .get(getOneItem) 20 | .post(madeItemPurchase); 21 | router 22 | .route("/:itemId/registry/:registryId") 23 | .put(verifyToken, checkOwnership, updateOneItem) 24 | .delete(verifyToken, checkOwnership, deleteOneItem); 25 | router 26 | .route("/registry/:registryId") 27 | .get(verifyToken, checkOwnership, getItemsByRegistry) 28 | .post(verifyToken, createItem) 29 | .delete(verifyToken, checkOwnership, deleteMultipleItems); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /backend/src/routes/Items/items.test.ts: -------------------------------------------------------------------------------- 1 | import { setup, mockControllers } from "../../test"; 2 | import { validItem, invalidItem } from "../../models/Item"; 3 | import { validRegistry } from "../../models/Registry"; 4 | import { purchaseItem } from "../../test/mockcontrollers"; 5 | 6 | const { 7 | createItem, 8 | createRegistry, 9 | deleteItem, 10 | deleteManyItems, 11 | getAllItems, 12 | getItem, 13 | getRegistrybyUrl, 14 | updateItem, 15 | } = mockControllers; 16 | 17 | describe("Item Endpoint Tests", () => { 18 | const tokens = setup(); 19 | 20 | describe("Item Creation", () => { 21 | test("expected to successfully create one item", async () => { 22 | const token = tokens.validTokenPaidUser; 23 | 24 | const registry = await createRegistry(validRegistry, 201, token); 25 | const registryId = registry.body._id; 26 | 27 | const resp = await createItem(registryId, validItem, 201, token); 28 | expect(resp.body.name).toBe(validItem.name); 29 | expect(resp.body.description).toBe(validItem.description); 30 | expect(resp.body.price).toBe(validItem.price); 31 | }); 32 | 33 | test("expected to unsuccessfully create one item", async () => { 34 | const token = tokens.validTokenPaidUser; 35 | 36 | const registry = await createRegistry(validRegistry, 201, token); 37 | const registryId = registry.body._id; 38 | 39 | const resp = await createItem(registryId, invalidItem, 400, token); 40 | expect(resp.body.message).toBe( 41 | "Item validation failed: description: Item description required, price: Path `price` (-1) is less than minimum allowed value (0)." 42 | ); 43 | }); 44 | }); 45 | 46 | describe("Get Item/s", () => { 47 | test("expected to get all items in DB", async () => { 48 | const token = tokens.validTokenPaidUser; 49 | 50 | const registry = await createRegistry(validRegistry, 201, token); 51 | const registryId = registry.body._id; 52 | 53 | const newItem1 = await createItem(registryId, validItem, 201, token); 54 | const newItem2 = await createItem(registryId, validItem, 201, token); 55 | 56 | const allItems = await getAllItems(); 57 | expect(allItems.body).toHaveLength(2); 58 | expect(allItems.body).toStrictEqual([newItem1.body, newItem2.body]); 59 | }); 60 | 61 | test("expected no items in test DB", async () => { 62 | const allItems = await getAllItems(); 63 | expect(allItems.body).toHaveLength(0); 64 | }); 65 | 66 | test("expected to get a single item by itemID", async () => { 67 | const token = tokens.validTokenPaidUser; 68 | 69 | const registry = await createRegistry(validRegistry, 201, token); 70 | const registryId = registry.body._id; 71 | 72 | const newItem = await createItem(registryId, validItem, 201, token); 73 | const itemId = newItem.body._id; 74 | 75 | const foundItem = await getItem(itemId, 200); 76 | expect(foundItem.body).not.toBeNull(); 77 | // could check each field invidiually here 78 | expect(foundItem.body._id).toBe(newItem.body._id); 79 | expect(foundItem.body.name).toBe(newItem.body.name); 80 | expect(foundItem.body.description).toBe(newItem.body.description); 81 | expect(foundItem.body.price).toBe(newItem.body.price); 82 | }); 83 | 84 | test("expected to not find a single invalid item", async () => { 85 | const item = await getItem("invalid-id-string", 400); 86 | expect(item.body.message).toBe( 87 | // eslint-disable-next-line quotes 88 | 'Cast to ObjectId failed for value "invalid-id-string" at path "_id" for model "Item"' 89 | ); 90 | }); 91 | }); 92 | 93 | describe("Update/Delete Items", () => { 94 | test("expected to delete a single item", async () => { 95 | const token = tokens.validTokenPaidUser; 96 | 97 | const registry = await createRegistry(validRegistry, 201, token); 98 | const registryId = registry.body._id; 99 | 100 | const newItem = await createItem(registryId, validItem, 201, token); 101 | const itemId = newItem.body._id; 102 | 103 | const foundItem = await getItem(itemId, 200); 104 | expect(foundItem.body).not.toBeNull(); 105 | // could check each field invidiually here 106 | expect(foundItem.body._id).toBe(newItem.body._id); 107 | expect(foundItem.body.name).toBe(newItem.body.name); 108 | expect(foundItem.body.description).toBe(newItem.body.description); 109 | expect(foundItem.body.price).toBe(newItem.body.price); 110 | 111 | const deletedItem = await deleteItem(itemId, registryId, 200, token); 112 | expect(deletedItem.body).not.toBeNull(); 113 | 114 | const resp = await getItem(itemId, 404); 115 | expect(resp.body.message).toBe(`Item (${itemId}) not found`); 116 | }); 117 | 118 | test("expected to unsuccessfully delete a single item", async () => { 119 | const token = tokens.validTokenPaidUser; 120 | 121 | const registry = await createRegistry(validRegistry, 201, token); 122 | const registryId = registry.body._id; 123 | 124 | const newItem = await createItem(registryId, validItem, 201, token); 125 | const itemId = newItem.body._id; 126 | 127 | const foundItem = await getItem(itemId, 200); 128 | expect(foundItem.body).not.toBeNull(); 129 | // could check each field invidiually here 130 | expect(foundItem.body._id).toBe(newItem.body._id); 131 | expect(foundItem.body.name).toBe(newItem.body.name); 132 | expect(foundItem.body.description).toBe(newItem.body.description); 133 | expect(foundItem.body.price).toBe(newItem.body.price); 134 | 135 | const resp = await deleteItem( 136 | "invalid-id-string", 137 | registryId, 138 | 400, 139 | token 140 | ); 141 | expect(resp.body.message).toBe( 142 | // eslint-disable-next-line quotes 143 | 'Cast to ObjectId failed for value "invalid-id-string" at path "_id" for model "Item"' 144 | ); 145 | }); 146 | 147 | test("expected to successfully delete multiple items", async () => { 148 | const token = tokens.validTokenPaidUser; 149 | 150 | const registry = await createRegistry(validRegistry, 201, token); 151 | const registryId = registry.body._id; 152 | const registryUrl = registry.body.customUrl; 153 | 154 | const newItem1 = await createItem(registryId, validItem, 201, token); 155 | const newItem2 = await createItem(registryId, validItem, 201, token); 156 | const itemId1 = newItem1.body._id; 157 | const itemId2 = newItem2.body._id; 158 | const arrayOfIds = JSON.stringify([itemId1, itemId2]); 159 | 160 | const resp = await deleteManyItems(registryId, arrayOfIds, 200, token); 161 | expect(resp.body.message).toBe( 162 | "Updated registry items and deleted 2/2 items" 163 | ); 164 | 165 | // check that the items don't exist now 166 | const item1 = await getItem(itemId1, 404); 167 | const item2 = await getItem(itemId2, 404); 168 | expect(item1.body.message).toBe(`Item (${itemId1}) not found`); 169 | expect(item2.body.message).toBe(`Item (${itemId2}) not found`); 170 | 171 | // check that the items ids aren't in the registry items array 172 | const updatedRegistry = await getRegistrybyUrl(registryUrl, 200); 173 | expect(updatedRegistry.body.items).not.toContain(itemId1); 174 | expect(updatedRegistry.body.items).not.toContain(itemId2); 175 | }); 176 | 177 | test("expected to unsuccessfully delete multiple items - missing arrayOfIds", async () => { 178 | const token = tokens.validTokenPaidUser; 179 | 180 | const registry = await createRegistry(validRegistry, 201, token); 181 | const registryId = registry.body._id; 182 | 183 | await createItem(registryId, validItem, 201, token); 184 | await createItem(registryId, validItem, 201, token); 185 | 186 | const resp = await deleteManyItems(registryId, "", 400, token); 187 | expect(resp.body.message).toBe("Please pass arrayOfIds in the body"); 188 | }); 189 | 190 | test("expected to unsuccessfully delete multiple items - empty arrayOfIds", async () => { 191 | const token = tokens.validTokenPaidUser; 192 | 193 | const registry = await createRegistry(validRegistry, 201, token); 194 | const registryId = registry.body._id; 195 | 196 | await createItem(registryId, validItem, 201, token); 197 | await createItem(registryId, validItem, 201, token); 198 | const arrayOfIds = JSON.stringify([]); 199 | 200 | const resp = await deleteManyItems(registryId, arrayOfIds, 400, token); 201 | expect(resp.body.message).toBe("You didn't put any _id's in arrayOfIds"); 202 | }); 203 | 204 | test("expected to unsuccessfully delete multiple items - invalid id in arrayOfIds", async () => { 205 | const token = tokens.validTokenPaidUser; 206 | 207 | const registry = await createRegistry(validRegistry, 201, token); 208 | const registryId = registry.body._id; 209 | 210 | const newItem1 = await createItem(registryId, validItem, 201, token); 211 | await createItem(registryId, validItem, 201, token); 212 | const itemId1 = newItem1.body._id; 213 | const arrayOfIds = JSON.stringify([itemId1, "invalidId"]); 214 | 215 | const resp = await deleteManyItems(registryId, arrayOfIds, 400, token); 216 | expect(resp.body.message).toBe( 217 | "Argument passed in must be a single String of 12 bytes or a string of 24 hex characters" 218 | ); 219 | }); 220 | 221 | test("expected to unsuccessfully delete multiple items - invalid registry", async () => { 222 | const token = tokens.validTokenPaidUser; 223 | 224 | const registry = await createRegistry(validRegistry, 201, token); 225 | const registryId = registry.body._id; 226 | const invalidRegistryId = registryId + "2"; 227 | 228 | const newItem1 = await createItem(registryId, validItem, 201, token); 229 | const newItem2 = await createItem(registryId, validItem, 201, token); 230 | const itemId1 = newItem1.body._id; 231 | const itemId2 = newItem2.body._id; 232 | const arrayOfIds = JSON.stringify([itemId1, itemId2]); 233 | 234 | const resp = await deleteManyItems( 235 | invalidRegistryId, 236 | arrayOfIds, 237 | 400, 238 | token 239 | ); 240 | expect(resp.body.message).toBe( 241 | `Cast to ObjectId failed for value \"${invalidRegistryId}\" at path \"_id\" for model \"Registry\"` 242 | ); 243 | }); 244 | 245 | test("expected to successfully update an item", async () => { 246 | const token = tokens.validTokenPaidUser; 247 | 248 | const registry = await createRegistry(validRegistry, 201, token); 249 | const registryId = registry.body._id; 250 | 251 | const newItem = await createItem(registryId, validItem, 201, token); 252 | const itemId = newItem.body._id; 253 | 254 | const foundItem = await getItem(itemId, 200); 255 | expect(foundItem.body).not.toBeNull(); 256 | // could check each field invidiually here 257 | expect(foundItem.body._id).toBe(newItem.body._id); 258 | expect(foundItem.body.name).toBe(newItem.body.name); 259 | expect(foundItem.body.description).toBe(newItem.body.description); 260 | expect(foundItem.body.price).toBe(newItem.body.price); 261 | 262 | const updateObj = { 263 | name: "New BMW", 264 | description: "Gonna be the next transporter", 265 | price: 40000, 266 | }; 267 | const updatedItem = await updateItem( 268 | itemId, 269 | registryId, 270 | updateObj, 271 | 200, 272 | token 273 | ); 274 | expect(updatedItem.body.name).toBe(updateObj.name); 275 | expect(updatedItem.body.description).toBe(updateObj.description); 276 | expect(updatedItem.body.price).toBe(updateObj.price); 277 | }); 278 | 279 | test("expected to unsuccessfully update an item", async () => { 280 | const token = tokens.validTokenPaidUser; 281 | 282 | const registry = await createRegistry(validRegistry, 201, token); 283 | const registryId = registry.body._id; 284 | 285 | const newItem = await createItem(registryId, validItem, 201, token); 286 | const itemId = newItem.body._id; 287 | 288 | const foundItem = await getItem(itemId, 200); 289 | expect(foundItem.body).not.toBeNull(); 290 | // could check each field invidiually here 291 | expect(foundItem.body._id).toBe(newItem.body._id); 292 | expect(foundItem.body.name).toBe(newItem.body.name); 293 | expect(foundItem.body.description).toBe(newItem.body.description); 294 | expect(foundItem.body.price).toBe(newItem.body.price); 295 | 296 | const error = await updateItem( 297 | itemId, 298 | registryId, 299 | { price: "BMW" }, 300 | 400, 301 | token 302 | ); 303 | 304 | expect(error.body.message).toBe( 305 | // eslint-disable-next-line quotes 306 | 'Cast to number failed for value "BMW" at path "price"' 307 | ); 308 | }); 309 | 310 | test("expected to successfully make an item 'purchase'", async () => { 311 | const token = tokens.validTokenPaidUser; 312 | 313 | const registry = await createRegistry(validRegistry, 201, token); 314 | const registryId = registry.body._id; 315 | 316 | const newItem = await createItem(registryId, validItem, 201, token); 317 | const itemId = newItem.body._id; 318 | 319 | const foundItem = await getItem(itemId, 200); 320 | expect(foundItem.body).not.toBeNull(); 321 | // could check each field invidiually here 322 | expect(foundItem.body._id).toBe(newItem.body._id); 323 | expect(foundItem.body.name).toBe(newItem.body.name); 324 | expect(foundItem.body.description).toBe(newItem.body.description); 325 | expect(foundItem.body.price).toBe(newItem.body.price); 326 | 327 | const purchaseInfo = { 328 | name: "Mr Bean", 329 | pricePaid: 100, 330 | }; 331 | 332 | const updatedItem = await purchaseItem(itemId, purchaseInfo, 200); 333 | expect(updatedItem.body.totalPurchased).toBe(purchaseInfo.pricePaid); 334 | }); 335 | 336 | test("expected to unsuccessfully make an item 'purchase' - pricePaid", async () => { 337 | const token = tokens.validTokenPaidUser; 338 | 339 | const registry = await createRegistry(validRegistry, 201, token); 340 | const registryId = registry.body._id; 341 | 342 | const newItem = await createItem(registryId, validItem, 201, token); 343 | const itemId = newItem.body._id; 344 | 345 | const foundItem = await getItem(itemId, 200); 346 | expect(foundItem.body).not.toBeNull(); 347 | // could check each field invidiually here 348 | expect(foundItem.body._id).toBe(newItem.body._id); 349 | expect(foundItem.body.name).toBe(newItem.body.name); 350 | expect(foundItem.body.description).toBe(newItem.body.description); 351 | expect(foundItem.body.price).toBe(newItem.body.price); 352 | 353 | const purchaseInfo = { 354 | name: "Mr Bean", 355 | }; 356 | 357 | const updatedItem = await purchaseItem(itemId, purchaseInfo, 400); 358 | expect(updatedItem.body.message).toBe( 359 | "You forgot to pass in the pricePaid" 360 | ); 361 | }); 362 | }); 363 | }); 364 | -------------------------------------------------------------------------------- /backend/src/routes/Registry/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./registry-routes"; 2 | -------------------------------------------------------------------------------- /backend/src/routes/Registry/registry-controllers.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import createError from "http-errors"; 3 | import { Registry, Item } from "../../models"; 4 | import { AuthHandler, structureItem } from "../../utils"; 5 | import { ItemI } from "../../models/Item/item-types"; 6 | 7 | export const getEveryRegistry: RequestHandler = async (_req, res, next) => { 8 | try { 9 | const registry = await Registry.find().lean(); 10 | console.log("registries"); 11 | res.status(200).json(registry); 12 | } catch (err) { 13 | next(err); 14 | } 15 | }; 16 | 17 | export const createRegistry: AuthHandler = async (req, res, next) => { 18 | try { 19 | const userId = req.user?.sub; 20 | 21 | if (!userId) throw createError(404, "User is not valid"); 22 | 23 | const newRegistry = await Registry.create({ 24 | ...req.body, 25 | userId, 26 | }); 27 | 28 | res.status(201).json(newRegistry); 29 | } catch (err) { 30 | next(err); 31 | } 32 | }; 33 | 34 | export const getMyRegistry: AuthHandler = async (req, res, next) => { 35 | try { 36 | const userId = req.user?.sub; 37 | 38 | if (!userId) throw createError(404, "User is not valid"); 39 | 40 | const registry = await Registry.findOne({ userId }) 41 | .populate("items") 42 | .lean(); 43 | if (!registry) throw createError(404, "You don't have a registry"); 44 | 45 | res.status(200).json(registry); 46 | } catch (err) { 47 | next(err); 48 | } 49 | }; 50 | 51 | export const getOneRegistry: RequestHandler = async (req, res, next) => { 52 | try { 53 | const { customUrl } = req.params; 54 | 55 | const registry = await Registry.findOne( 56 | { customUrl }, 57 | { email: 0, phoneNumber: 0, userId: 0 } 58 | ) 59 | .populate("items") 60 | .lean(); 61 | if (!registry) throw createError(404, `Registry (${customUrl}) not found`); 62 | 63 | registry.items = registry.items.map((item: ItemI) => structureItem(item)); 64 | 65 | res.status(200).json(registry); 66 | } catch (err) { 67 | next(err); 68 | } 69 | }; 70 | 71 | export const updateOneRegistry: RequestHandler = async (req, res, next) => { 72 | try { 73 | const { registryId } = req.params; 74 | 75 | // update a registry based on it's _id 76 | // anything sent in the body will overwrite the given values 77 | // will throw an error if validations don't succeed 78 | const updatedRegistry = await Registry.findByIdAndUpdate( 79 | registryId, 80 | req.body, 81 | { 82 | new: true, 83 | runValidators: true, 84 | } 85 | ) 86 | .lean() 87 | .populate("items"); 88 | if (!updatedRegistry) { 89 | throw createError(400, `Error updating Registry (${registryId})`); 90 | } 91 | 92 | res.status(200).json(updatedRegistry); 93 | } catch (err) { 94 | next(err); 95 | } 96 | }; 97 | 98 | export const deleteOneRegistry: RequestHandler = async (req, res, next) => { 99 | try { 100 | const { registryId } = req.params; 101 | 102 | // delete the given registry based on it's _id 103 | const deletedRegistry = await Registry.findByIdAndDelete(registryId); 104 | if (!deletedRegistry) { 105 | throw createError(400, `Error removing registry (${registryId})`); 106 | } 107 | 108 | // delete all the items that are inside the recently deleted registry 109 | const { deletedCount, n } = await Item.deleteMany({ 110 | _id: { 111 | $in: deletedRegistry.items, 112 | }, 113 | }); 114 | const message = `Deleted registry and ${deletedCount}/${n} item${ 115 | n === 1 ? "" : "s" 116 | }`; 117 | 118 | res.status(200).json({ message }); 119 | } catch (err) { 120 | next(err); 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /backend/src/routes/Registry/registry-routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { verifyToken, checkOwnership } from "../../middleware"; 3 | import { 4 | getEveryRegistry, 5 | createRegistry, 6 | getOneRegistry, 7 | updateOneRegistry, 8 | deleteOneRegistry, 9 | getMyRegistry, 10 | } from "./registry-controllers"; 11 | 12 | const router = Router(); 13 | 14 | router.get("/", getEveryRegistry); 15 | router.post("/", verifyToken, createRegistry); 16 | router.get("/admin", verifyToken, getMyRegistry); 17 | router.get("/:customUrl", getOneRegistry); 18 | router.put("/:registryId", verifyToken, checkOwnership, updateOneRegistry); 19 | router.delete("/:registryId", verifyToken, checkOwnership, deleteOneRegistry); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /backend/src/routes/Registry/registry.test.ts: -------------------------------------------------------------------------------- 1 | import { setup, mockControllers } from "../../test"; 2 | import { validRegistry, invalidRegistry } from "../../models/Registry"; 3 | 4 | const { 5 | getAllRegistries, 6 | getRegistrybyUrl, 7 | createRegistry, 8 | deleteRegistry, 9 | updateOneRegistry, 10 | } = mockControllers; 11 | 12 | describe("Registry Endpoint Test", () => { 13 | const tokens = setup(); 14 | 15 | describe("Registry Creation", () => { 16 | test("expected to successfully create one registry", async () => { 17 | const token = tokens.validTokenPaidUser; 18 | 19 | const resp = await createRegistry(validRegistry, 201, token); 20 | expect(resp.body.title).toBe(validRegistry.title); 21 | expect(resp.body.description).toBe(validRegistry.description); 22 | expect(resp.body.p1FullName).toBe(validRegistry.p1FullName); 23 | expect(resp.body.p2FullName).toBe(validRegistry.p2FullName); 24 | expect(resp.body.email).toBe(validRegistry.email); 25 | expect(resp.body.userId).toBe(validRegistry.userId); 26 | expect(resp.body.customUrl).toBe(validRegistry.customUrl); 27 | }); 28 | 29 | test("expected to unsuccessfully create one registry", async () => { 30 | const token = tokens.validTokenPaidUser; 31 | 32 | const resp = await createRegistry(invalidRegistry, 400, token); 33 | expect(resp.body.message).toBe( 34 | "Registry validation failed: p1FullName: Registry p1FullName required, description: Registry description required, title: Registry title required" 35 | ); 36 | }); 37 | 38 | test("expected to unsuccessfully create a registry with the same customUrl", async () => { 39 | const token = tokens.validTokenPaidUser; 40 | 41 | const newRegistry1 = await createRegistry(validRegistry, 201, token); 42 | const newRegistry2 = await createRegistry(validRegistry, 400, token); 43 | expect(newRegistry2.body.message).toBe( 44 | `E11000 duplicate key error dup key: { : \"${newRegistry1.body.customUrl}"\ }` 45 | ); 46 | }); 47 | }); 48 | 49 | describe("Get Registry/s", () => { 50 | test("Expected to get all items in DB", async () => { 51 | const token = tokens.validTokenPaidUser; 52 | 53 | const newRegistry1 = await createRegistry(validRegistry, 201, token); 54 | const newRegistry2 = await createRegistry( 55 | { ...validRegistry, customUrl: "newRegistry2" }, 56 | 201, 57 | token 58 | ); 59 | 60 | const allRegistries = await getAllRegistries(); 61 | expect(allRegistries.body).toHaveLength(2); 62 | expect(allRegistries.body).toStrictEqual([ 63 | newRegistry1.body, 64 | newRegistry2.body, 65 | ]); 66 | }); 67 | 68 | test("expected no Registries in test DB", async () => { 69 | const allRegistries = await getAllRegistries(); 70 | expect(allRegistries.body).toHaveLength(0); 71 | }); 72 | 73 | test("expected to get a single registry by customUrl", async () => { 74 | const token = tokens.validTokenPaidUser; 75 | 76 | const newRegistry = await createRegistry(validRegistry, 201, token); 77 | const customUrl = newRegistry.body.customUrl; 78 | 79 | const foundRegistry = await getRegistrybyUrl(customUrl, 200); 80 | expect(foundRegistry.body).not.toBeNull(); 81 | // could check each field invidiually here 82 | expect(foundRegistry.body._id).toBe(newRegistry.body._id); 83 | expect(foundRegistry.body.title).toBe(newRegistry.body.title); 84 | expect(foundRegistry.body.description).toBe(newRegistry.body.description); 85 | }); 86 | 87 | test("expected to not find a single invalid registry", async () => { 88 | const token = tokens.validTokenPaidUser; 89 | 90 | const newRegistry = await createRegistry(validRegistry, 201, token); 91 | const customUrl = newRegistry.body.customUrl + "invalid"; 92 | 93 | const foundRegistry = await getRegistrybyUrl(customUrl, 404); 94 | expect(foundRegistry.body.message).toBe( 95 | `Registry (${customUrl}) not found` 96 | ); 97 | }); 98 | }); 99 | 100 | describe("Update/Delete Registry", () => { 101 | test("expected to successfully delete a single registry", async () => { 102 | const token = tokens.validTokenPaidUser; 103 | 104 | const newRegistry = await createRegistry(validRegistry, 201, token); 105 | const registryId = newRegistry.body._id; 106 | const customUrl = newRegistry.body.customUrl; 107 | 108 | const foundRegistry = await getRegistrybyUrl(customUrl, 200); 109 | expect(foundRegistry.body).not.toBeNull(); 110 | // could check each field invidiually here 111 | expect(foundRegistry.body._id).toBe(newRegistry.body._id); 112 | expect(foundRegistry.body.title).toBe(newRegistry.body.title); 113 | expect(foundRegistry.body.description).toBe(newRegistry.body.description); 114 | 115 | const deletedRegistry = await deleteRegistry(registryId, 200, token); 116 | expect(deletedRegistry.body).not.toBeNull(); 117 | 118 | const resp = await getRegistrybyUrl(customUrl, 404); 119 | expect(resp.body.message).toBe(`Registry (${customUrl}) not found`); 120 | }); 121 | 122 | test("expected to unsuccessfully delete a single registry", async () => { 123 | const token = tokens.validTokenPaidUser; 124 | 125 | const newRegistry = await createRegistry(validRegistry, 201, token); 126 | const customUrl = newRegistry.body.customUrl; 127 | 128 | const foundRegistry = await getRegistrybyUrl(customUrl, 200); 129 | expect(foundRegistry.body).not.toBeNull(); 130 | // could check each field invidiually here 131 | expect(foundRegistry.body._id).toBe(newRegistry.body._id); 132 | expect(foundRegistry.body.title).toBe(newRegistry.body.title); 133 | expect(foundRegistry.body.description).toBe(newRegistry.body.description); 134 | 135 | const deletedRegistry = await deleteRegistry( 136 | "invalid-id-string", 137 | 400, 138 | token 139 | ); 140 | expect(deletedRegistry.body.message).toBe( 141 | // eslint-disable-next-line quotes 142 | 'Cast to ObjectId failed for value "invalid-id-string" at path "_id" for model "Registry"' 143 | ); 144 | }); 145 | 146 | test("expected to successfully update a single registry", async () => { 147 | const token = tokens.validTokenPaidUser; 148 | 149 | const newRegistry = await createRegistry(validRegistry, 201, token); 150 | const registryId = newRegistry.body._id; 151 | const customUrl = newRegistry.body.customUrl; 152 | 153 | const foundRegistry = await getRegistrybyUrl(customUrl, 200); 154 | expect(foundRegistry.body).not.toBeNull(); 155 | // could check each field invidiually here 156 | expect(foundRegistry.body._id).toBe(newRegistry.body._id); 157 | expect(foundRegistry.body.title).toBe(newRegistry.body.title); 158 | expect(foundRegistry.body.description).toBe(newRegistry.body.description); 159 | 160 | const updateObj = { 161 | title: "New Title", 162 | description: "New Description", 163 | customUrl: "coolNewUrl", 164 | }; 165 | const updatedRegistry = await updateOneRegistry( 166 | registryId, 167 | updateObj, 168 | 200, 169 | token 170 | ); 171 | 172 | expect(updatedRegistry.body.title).toBe(updateObj.title); 173 | expect(updatedRegistry.body.description).toBe(updateObj.description); 174 | expect(updatedRegistry.body.customUrl).toBe(updateObj.customUrl); 175 | }); 176 | 177 | test("expected to unsuccessfully update a single registry", async () => { 178 | const token = tokens.validTokenPaidUser; 179 | 180 | const newRegistry = await createRegistry(validRegistry, 201, token); 181 | const invalidRegistryId = newRegistry.body._id + "-invalid"; 182 | const customUrl = newRegistry.body.customUrl; 183 | 184 | const foundRegistry = await getRegistrybyUrl(customUrl, 200); 185 | expect(foundRegistry.body).not.toBeNull(); 186 | // could check each field invidiually here 187 | expect(foundRegistry.body._id).toBe(newRegistry.body._id); 188 | expect(foundRegistry.body.title).toBe(newRegistry.body.title); 189 | expect(foundRegistry.body.description).toBe(newRegistry.body.description); 190 | 191 | const updateObj = { 192 | title: "New Title", 193 | description: "New Description", 194 | customUrl: "coolNewUrl", 195 | }; 196 | const error = await updateOneRegistry( 197 | invalidRegistryId, 198 | updateObj, 199 | 400, 200 | token 201 | ); 202 | 203 | expect(error.body.message).toBe( 204 | `Cast to ObjectId failed for value "${invalidRegistryId}" at path "_id" for model "Registry"` 205 | ); 206 | }); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /backend/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import ItemRoutes from "./Items"; 3 | import RegistryRoutes from "./Registry"; 4 | 5 | const router = Router(); 6 | 7 | // Index Route -- /api 8 | router.get("/", (_, res) => res.send("v16 Bears 4 API")); 9 | router.use("/item", ItemRoutes); 10 | router.use("/registry", RegistryRoutes); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /backend/src/server/app.test.ts: -------------------------------------------------------------------------------- 1 | import { request } from "../test"; 2 | 3 | const invalidRoute = "/noapi"; 4 | 5 | describe("App Tests", () => { 6 | test(`expected unsuccessful ${invalidRoute} route`, async () => { 7 | const resp = await request.get(invalidRoute).expect(404); 8 | expect(resp.body.message).toBe(`Invalid route: ${invalidRoute}`); 9 | }); 10 | 11 | test("expected successful '/api' route", async () => { 12 | const resp = await request.get("/api"); 13 | expect(resp.status).toBe(200); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /backend/src/server/app.ts: -------------------------------------------------------------------------------- 1 | import cors from "cors"; 2 | import express, { ErrorRequestHandler } from "express"; 3 | import createError from "http-errors"; 4 | import morganBody from "morgan-body"; 5 | import routes from "../routes"; 6 | // import { requireAuth } from "../middleware"; 7 | 8 | const app = express(); 9 | 10 | // ENVIRONMENT VARIABLES GO HERE 11 | const NODE_ENV = process.env.NODE_ENV; 12 | 13 | // BEAUTIFIES REQUEST AND RESPONSE BODIES 14 | if (NODE_ENV === "development" || NODE_ENV === "test:withLogs") { 15 | morganBody(app, { theme: "darkened", dateTimeFormat: "utc" }); 16 | } 17 | 18 | // EXPRESS MIDDLEWARES 19 | app.use( 20 | cors({ 21 | origin: 22 | NODE_ENV === "development" 23 | ? "http://localhost:3000" 24 | : "https://bears04.now.sh", 25 | }) 26 | ); 27 | app.use(express.json()); 28 | app.use(express.urlencoded({ extended: false })); 29 | 30 | // use all routes exported from the routes folder 31 | app.use("/api", routes); 32 | 33 | // used to catch any routes not found 34 | app.use((req, _, next) => { 35 | next(createError(404, `Invalid route: ${req.url}`)); 36 | }); 37 | 38 | // global error handler 39 | // access by passing err with the next callback ie) next(err) 40 | const GlobalErrorHandler: ErrorRequestHandler = ( 41 | { status, message }: { status: number; message: string }, 42 | _req, 43 | res, 44 | _next 45 | ) => { 46 | res.status(status || 400).json({ message }); 47 | }; 48 | app.use(GlobalErrorHandler); 49 | 50 | export default app; 51 | -------------------------------------------------------------------------------- /backend/src/server/database.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const NODE_ENV = process.env.NODE_ENV; 4 | const password = process.env.DB_PASSWORD; 5 | const user = process.env.DB_USER; 6 | const url = process.env.DB_URL; 7 | 8 | const mongoURL = `mongodb+srv://${user}:${password}@${url}${NODE_ENV}?retryWrites=true&w=majority`; 9 | const mongoMsg = (msg: string) => console.log(`DB connection ${msg}`); 10 | const mongoOpts = { 11 | useNewUrlParser: true, 12 | useUnifiedTopology: true, 13 | useFindAndModify: false, 14 | useCreateIndex: true, 15 | }; 16 | 17 | function connectToDB() { 18 | mongoose 19 | .connect(mongoURL, mongoOpts) 20 | .then(() => mongoMsg("established")) 21 | .catch((err: string) => mongoMsg(`failed - (${err})`)); 22 | 23 | mongoose.connection.on("error", err => mongoMsg(`severed - (${err})`)); 24 | } 25 | 26 | export default connectToDB; 27 | export { mongoOpts }; 28 | -------------------------------------------------------------------------------- /backend/src/server/index.ts: -------------------------------------------------------------------------------- 1 | import app from "./app"; 2 | import connectToDB from "./database"; 3 | 4 | const PORT = process.env.PORT || 5000; 5 | 6 | connectToDB(); 7 | 8 | app.listen(PORT, () => console.log(`Server listening on port: ${PORT}`)); 9 | -------------------------------------------------------------------------------- /backend/src/test/index.ts: -------------------------------------------------------------------------------- 1 | import request from "./request"; 2 | import setup from "./setup"; 3 | import * as mockControllers from "./mockcontrollers"; 4 | 5 | export { request, setup, mockControllers }; 6 | -------------------------------------------------------------------------------- /backend/src/test/mockcontrollers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mock-items"; 2 | export * from "./mock-registry"; 3 | -------------------------------------------------------------------------------- /backend/src/test/mockcontrollers/mock-items.ts: -------------------------------------------------------------------------------- 1 | import request from "../request"; 2 | 3 | export async function getAllItems() { 4 | return await request.get("/api/item/all").expect(200); 5 | } 6 | 7 | export async function getItem(itemId: string, status: number) { 8 | return await request.get(`/api/item/${itemId}`).expect(status); 9 | } 10 | 11 | export async function createItem( 12 | registryId: string, 13 | item: {}, 14 | status: number, 15 | token: string 16 | ) { 17 | return await request 18 | .post(`/api/item/registry/${registryId}`) 19 | .send(item) 20 | .set("Authorization", `Bearer ${token}`) 21 | .expect(status); 22 | } 23 | 24 | export async function deleteItem( 25 | itemId: string, 26 | registryId: string, 27 | status: number, 28 | token: string 29 | ) { 30 | return await request 31 | .delete(`/api/item/${itemId}/registry/${registryId}`) 32 | .set("Authorization", `Bearer ${token}`) 33 | .expect(status); 34 | } 35 | 36 | export async function deleteManyItems( 37 | registryId: string, 38 | arrayOfIds: string | undefined, 39 | status: number, 40 | token: string 41 | ) { 42 | return await request 43 | .delete(`/api/item/registry/${registryId}`) 44 | .send({ arrayOfIds }) 45 | .set("Authorization", `Bearer ${token}`) 46 | .expect(status); 47 | } 48 | 49 | export async function updateItem( 50 | itemId: string, 51 | registryId: string, 52 | item: {}, 53 | status: number, 54 | token: string 55 | ) { 56 | return await request 57 | .put(`/api/item/${itemId}/registry/${registryId}`) 58 | .send(item) 59 | .set("Authorization", `Bearer ${token}`) 60 | .expect(status); 61 | } 62 | 63 | export async function purchaseItem( 64 | itemId: string, 65 | purchaseInfo: {}, 66 | status: number 67 | ) { 68 | return await request 69 | .post(`/api/item/${itemId}`) 70 | .send(purchaseInfo) 71 | .expect(status); 72 | } 73 | -------------------------------------------------------------------------------- /backend/src/test/mockcontrollers/mock-registry.ts: -------------------------------------------------------------------------------- 1 | import request from "../request"; 2 | 3 | export async function getAllRegistries() { 4 | return await request.get("/api/registry").expect(200); 5 | } 6 | 7 | export async function getRegistrybyUrl(customUrl: string, status: number) { 8 | return await request.get(`/api/registry/${customUrl}`).expect(status); 9 | } 10 | 11 | export async function createRegistry( 12 | registry: {}, 13 | status: number, 14 | token: string 15 | ) { 16 | return await request 17 | .post("/api/registry") 18 | .send(registry) 19 | .set("Authorization", `Bearer ${token}`) 20 | .expect(status); 21 | } 22 | 23 | export async function deleteRegistry( 24 | registryId: string, 25 | status: number, 26 | token: string 27 | ) { 28 | return await request 29 | .delete(`/api/registry/${registryId}`) 30 | .set("Authorization", `Bearer ${token}`) 31 | .expect(status); 32 | } 33 | 34 | export async function updateOneRegistry( 35 | registryId: string, 36 | registry: {}, 37 | status: number, 38 | token: string 39 | ) { 40 | return await request 41 | .put(`/api/registry/${registryId}`) 42 | .send(registry) 43 | .set("Authorization", `Bearer ${token}`) 44 | .expect(status); 45 | } 46 | -------------------------------------------------------------------------------- /backend/src/test/request.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import app from "../server/app"; 3 | 4 | const request = supertest(app); 5 | 6 | export default request; 7 | -------------------------------------------------------------------------------- /backend/src/test/setup/authsetup.ts: -------------------------------------------------------------------------------- 1 | import createJWKSMock from "mock-jwks"; 2 | import * as token from "../../utils"; 3 | 4 | export default function authSetup() { 5 | const jwksMock = createJWKSMock(`https://${process.env.AUTH0_DOMAIN}/`); 6 | 7 | const validTokenAdmin = jwksMock.token(token.validTokenAdmin); 8 | const validTokenPaidUser = jwksMock.token(token.validTokenPaidUser); 9 | const invalidTokenAdmin = jwksMock.token(token.invalidTokenAdmin); 10 | const invalidTokenPaidUser = jwksMock.token(token.invalidTokenPaidUser); 11 | 12 | beforeEach(() => jwksMock.start()); 13 | 14 | afterEach(async () => await jwksMock.stop()); 15 | 16 | return { 17 | validTokenAdmin, 18 | validTokenPaidUser, 19 | invalidTokenAdmin, 20 | invalidTokenPaidUser, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/test/setup/dbsetup.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { MongoMemoryServer } from "mongodb-memory-server"; 3 | import { mongoOpts } from "../../server/database"; 4 | 5 | const mongod = new MongoMemoryServer(); 6 | 7 | // Connect to the in-memory database. 8 | const connectDB = async () => { 9 | const uri = await mongod.getConnectionString(); 10 | await mongoose.connect(uri, mongoOpts); 11 | }; 12 | 13 | // Drop database, close the connection and stop mongod. 14 | const closeDB = async () => { 15 | await mongoose.connection.dropDatabase(); 16 | await mongoose.connection.close(); 17 | await mongod.stop(); 18 | }; 19 | 20 | // Remove all the data for all db collections. 21 | const clearDB = async () => { 22 | const collections = mongoose.connection.collections; 23 | 24 | for (const key in collections) { 25 | const collection = collections[key]; 26 | await collection.deleteMany({}); 27 | } 28 | }; 29 | 30 | // can import this function when testing ... 31 | // ... routes to setup a test database 32 | export default function dbSetup() { 33 | beforeAll(async () => await connectDB()); 34 | afterEach(async () => await clearDB()); 35 | afterAll(async () => await closeDB()); 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/test/setup/index.ts: -------------------------------------------------------------------------------- 1 | import dbSetup from "./dbsetup"; 2 | import authSetup from "./authsetup"; 3 | 4 | export default function setup() { 5 | // sets up an independent mock DB 6 | dbSetup(); 7 | 8 | // sets up a token validation mocker 9 | // returns a list of valid and invalid tokens 10 | const tokens = authSetup(); 11 | 12 | return tokens; 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/utils/authhandler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | // extending types to access req.user without type errors 4 | interface AuthRequest extends Request { 5 | user?: { 6 | sub: string; 7 | permissions: string[]; 8 | }; 9 | } 10 | 11 | export type AuthHandler = ( 12 | req: AuthRequest, 13 | res: Response, 14 | next: NextFunction 15 | ) => Promise; 16 | -------------------------------------------------------------------------------- /backend/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthHandler } from "./authhandler"; 2 | export * from "./permissions"; 3 | export * from "./tokens"; 4 | export * from "./pricing"; 5 | export * from "./item"; 6 | -------------------------------------------------------------------------------- /backend/src/utils/item.ts: -------------------------------------------------------------------------------- 1 | import { ItemI } from "../models/Item"; 2 | import { totalPurchased } from "./pricing"; 3 | 4 | export const structureItem = (item: ItemI) => ({ 5 | _id: item._id, 6 | isReserved: item.isReserved, 7 | name: item.name, 8 | description: item.description, 9 | price: item.price, 10 | link: item.link, 11 | image: item.image, 12 | addedOn: item.addedOn, 13 | totalPurchased: totalPurchased(item.purchasers), 14 | }); 15 | -------------------------------------------------------------------------------- /backend/src/utils/permissions.ts: -------------------------------------------------------------------------------- 1 | import createError = require("http-errors"); 2 | 3 | const PERMISSIONS_ADMIN = process.env.PERMISSIONS_ADMIN; 4 | const PERMISSIONS_PAIDUSER = process.env.PERMISSIONS_PAIDUSER; 5 | 6 | // formats error authorizations messages 7 | function createErrMsg(re: string) { 8 | return createError(401, `Authorization Failed (re: ${re})`); 9 | } 10 | 11 | // creates array of permissions 12 | export const permissionsAdmin = JSON.parse(PERMISSIONS_ADMIN || "[]"); 13 | export const permissionsPaidUser = JSON.parse(PERMISSIONS_PAIDUSER || "[]"); 14 | 15 | // throws an error if unsuccessful 16 | export function checkPermissions( 17 | givenPerms: string[] | undefined, 18 | neededPerms: string[], 19 | errMsg: string = "invalid permissions" 20 | ) { 21 | // throw if there are no permissions 22 | if (!givenPerms || !givenPerms.length || !neededPerms.length) { 23 | throw createErrMsg("no permissions found"); 24 | } 25 | 26 | // check that the user has the correct permissions 27 | const isValidPermCheck = neededPerms.every(neededPerm => 28 | givenPerms.includes(neededPerm) 29 | ); 30 | 31 | // if it's not valid throw an error 32 | if (!isValidPermCheck) throw createErrMsg(errMsg); 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/utils/pricing.ts: -------------------------------------------------------------------------------- 1 | import { PurchaserI } from "../models/Item/item-types"; 2 | 3 | export const totalPurchased = (purchasers: PurchaserI[] | undefined) => { 4 | if (!purchasers) return 0; 5 | 6 | return purchasers.reduce( 7 | (total: number, purchaser: PurchaserI) => total + purchaser.pricePaid, 8 | 0 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /backend/src/utils/tokens.ts: -------------------------------------------------------------------------------- 1 | import { permissionsAdmin, permissionsPaidUser } from "./permissions"; 2 | 3 | const opts = { 4 | aud: "private", 5 | iss: "master", 6 | sub: "1234567890", 7 | }; 8 | 9 | export const validTokenAdmin = { 10 | ...opts, 11 | permissions: permissionsAdmin, 12 | }; 13 | 14 | export const validTokenPaidUser = { 15 | ...opts, 16 | permissions: permissionsPaidUser, 17 | }; 18 | 19 | export const invalidTokenAdmin = { 20 | ...opts, 21 | permissions: permissionsPaidUser, 22 | }; 23 | 24 | export const invalidTokenPaidUser = { 25 | ...opts, 26 | permissions: [], 27 | }; 28 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "allowSyntheticDefaultImports": true, 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist", 11 | "allowJs": true, 12 | "checkJs": false, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules", "src/test/*", "src/**/*.test.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "commonjs": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:prettier/recommended" 12 | ], 13 | "globals": { 14 | "Atomics": "readonly", 15 | "SharedArrayBuffer": "readonly" 16 | }, 17 | "parserOptions": { 18 | "ecmaFeatures": { 19 | "jsx": true 20 | }, 21 | "ecmaVersion": 2018, 22 | "sourceType": "module" 23 | }, 24 | "plugins": ["react"], 25 | "rules": {} 26 | } 27 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | package-lock.json 28 | .eslintcache 29 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "printWidth": 80, 5 | "semi": true, 6 | "singleQuote": false, 7 | "tabWidth": 2, 8 | "trailingComma": "es5" 9 | } 10 | -------------------------------------------------------------------------------- /frontend/components/AdminItemsTable.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer, useCallback } from "react"; 2 | import { mutate } from "swr"; 3 | import PropTypes from "prop-types"; 4 | import { useAuth } from "use-auth0-hooks"; 5 | import { itemType } from "../types"; 6 | import { adminFetchIt, AUTH0_API_IDENTIFIER } from "../utils"; 7 | import Button from "./Button"; 8 | import Modal from "./Modal"; 9 | import Link from "./Link"; 10 | import Loader from "./Loader"; 11 | import { useSnacks } from "./Snack"; 12 | 13 | const audience = AUTH0_API_IDENTIFIER; 14 | 15 | const initialState = { 16 | isConfirmOpen: false, 17 | deleteItemId: "", 18 | deleteItemName: "", 19 | }; 20 | 21 | const reducer = (state, action) => { 22 | const { type, deleteItemId, deleteItemName } = action; 23 | switch (type) { 24 | case "Open_Delete": 25 | return { ...state, isConfirmOpen: true, deleteItemId, deleteItemName }; 26 | case "Close_Delete": 27 | return initialState; 28 | default: 29 | return state; 30 | } 31 | }; 32 | 33 | export default function AdminItemsTable({ items }) { 34 | const { accessToken } = useAuth({ audience }); 35 | const [state, dispatch] = useReducer(reducer, initialState); 36 | const { openSnack } = useSnacks(); 37 | 38 | // state and props 39 | const { isConfirmOpen, deleteItemId, deleteItemName } = state; 40 | 41 | // closes the delete modal 42 | const handleDeleteClose = useCallback(() => { 43 | dispatch({ type: "Close_Delete" }); 44 | }, []); 45 | 46 | // opens the delete modal and sets the item to be deleted 47 | const handleDeleteOpen = (deleteItemId, deleteItemName) => { 48 | dispatch({ type: "Open_Delete", deleteItemId, deleteItemName }); 49 | }; 50 | 51 | // deletes item and updates our cache 52 | const handleDeletion = () => { 53 | mutate(["/registry/admin", accessToken], async registry => { 54 | try { 55 | // create the delete one item url here 56 | const url = `/item/${deleteItemId}/registry/${registry._id}`; 57 | // make the fetch request 58 | await adminFetchIt(url, accessToken, { method: "DELETE" }); 59 | // if it's successful ... 60 | // ... close the dialog box 61 | handleDeleteClose(); 62 | // ... remove the deleted item and update our items array 63 | const updatedItems = 64 | registry.items && 65 | registry.items.filter(item => item._id !== deleteItemId); 66 | // mutate the cache by sending the updated items 67 | openSnack("Success! We deleted that gift", "success"); 68 | return { ...registry, items: updatedItems }; 69 | } catch (err) { 70 | console.log(err); 71 | openSnack("Sorry! We couldn't delete that gift", "error"); 72 | handleDeleteClose(); 73 | return registry; 74 | } 75 | }); 76 | }; 77 | 78 | if (!items.length) { 79 | return ( 80 | 81 | 82 | Add Gift 83 | 84 | 85 | ); 86 | } 87 | 88 | return ( 89 | <> 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | {items.map(item => { 103 | return ( 104 | 105 | 108 | 109 | 110 | 111 | 123 | 124 | ); 125 | })} 126 | 127 |
ImageNameDescriptionPrice
106 | 107 | {item.name}{item.description}{item.price} 112 | Edit 113 | 114 | 122 |
128 | 129 | 130 |
131 |

132 | Are you sure that you want to remove {deleteItemName || "this gift"}{" "} 133 | from this list? 134 |

135 |
136 | 137 | 144 |
145 |
146 |
147 | 148 | 177 | 178 | ); 179 | } 180 | 181 | AdminItemsTable.propTypes = { 182 | items: PropTypes.arrayOf(PropTypes.shape(itemType)), 183 | }; 184 | -------------------------------------------------------------------------------- /frontend/components/AdminPage/AdminPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import useSWR from "swr"; 4 | import { useAuth } from "use-auth0-hooks"; 5 | 6 | import colors from "../../css/colors"; 7 | import { authType } from "../../types"; 8 | import { AUTH0_API_IDENTIFIER, adminFetchIt } from "../../utils"; 9 | 10 | import SideBar from "./SideBar"; 11 | import Nav from "./Nav"; 12 | import Footer from "../Footer"; 13 | 14 | const audience = AUTH0_API_IDENTIFIER; 15 | const fetcher = adminFetchIt; 16 | 17 | export default function AdminPage({ children }) { 18 | const { accessToken, user } = useAuth({ audience }); 19 | const { data } = useSWR(["/registry/admin", accessToken], { fetcher }); 20 | 21 | if (!user) return null; 22 | 23 | return ( 24 |
25 |
26 |
33 | 34 |
35 | 36 | 60 |
61 | ); 62 | } 63 | 64 | AdminPage.propTypes = { 65 | auth: authType, 66 | children: PropTypes.func.isRequired, 67 | }; 68 | 69 | AdminPage.Header = ({ icon, title }) => { 70 | const Icon = React.cloneElement(icon, { color: "#fff", size: 30 }); 71 | return ( 72 |
73 |
74 | {Icon} 75 |
76 |

77 | {title} 78 |

79 | 80 | 85 |
86 | ); 87 | }; 88 | 89 | AdminPage.displayName = "AdminPage"; 90 | 91 | AdminPage.Header.displayName = "AdminPage__Header"; 92 | AdminPage.Header.propTypes = { 93 | icon: PropTypes.node.isRequired, 94 | title: PropTypes.string.isRequired, 95 | }; 96 | 97 | AdminPage.Main = ({ children }) =>
{children}
; 98 | AdminPage.Main.displayName = "AdminPage__Main"; 99 | AdminPage.Main.propTypes = { children: PropTypes.node.isRequired }; 100 | -------------------------------------------------------------------------------- /frontend/components/AdminPage/Nav/Nav.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SearchIcon from "@iconscout/react-unicons/icons/uil-search"; 3 | import colors from "../../../css/colors"; 4 | import NavMenu from "./NavMenu"; 5 | 6 | export default function Header() { 7 | return ( 8 |
9 |
10 | 11 | Search feature coming soon... 12 |
13 | 14 | 15 | 16 | 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /frontend/components/AdminPage/Nav/NavMenu.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import AccountIcon from "@iconscout/react-unicons/icons/uil-invoice"; 3 | import ProfileIcon from "@iconscout/react-unicons/icons/uil-user-circle"; 4 | import LogoutIcon from "@iconscout/react-unicons/icons/uil-sign-out-alt"; 5 | import { useAuth } from "use-auth0-hooks"; 6 | import colors from "../../../css/colors"; 7 | import NavMenuLink from "./NavMenuLink"; 8 | 9 | export default function NavMenu() { 10 | const [isProfileDropdownOpen, setIsProfileDropdownOpen] = useState(false); 11 | const { user, logout } = useAuth(); 12 | 13 | const toggleProfileDropdown = () => setIsProfileDropdownOpen(state => !state); 14 | 15 | return ( 16 |
17 | Profile image 23 |
24 |
    25 | } /> 26 | } /> 27 | } 30 | handleClick={logout} 31 | /> 32 |
33 |
34 | 35 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /frontend/components/AdminPage/Nav/NavMenuLink.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export default function NavMenuLink({ text, icon, handleClick = () => {} }) { 5 | return ( 6 |
  • 7 | {icon} 8 | {text} 9 | 10 | 23 |
  • 24 | ); 25 | } 26 | 27 | NavMenuLink.propTypes = { 28 | text: PropTypes.string.isRequired, 29 | icon: PropTypes.node.isRequired, 30 | handleClick: PropTypes.func, 31 | }; 32 | -------------------------------------------------------------------------------- /frontend/components/AdminPage/Nav/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Nav"; 2 | -------------------------------------------------------------------------------- /frontend/components/AdminPage/SideBar/SideBar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import MenuIcon from "@iconscout/react-unicons/icons/uil-bars"; 4 | import CloseMenuIcon from "@iconscout/react-unicons/icons/uil-multiply"; 5 | import colors from "../../../css/colors"; 6 | import SideBarLinks from "./SideBarLinks"; 7 | 8 | export default function SideBar({ data }) { 9 | const [isNavOpen, setIsNavOpen] = useState(false); 10 | 11 | return ( 12 | <> 13 |
    setIsNavOpen(true)}> 14 | 15 |
    16 | 17 | 29 | 30 | 86 | 87 | ); 88 | } 89 | 90 | SideBar.propTypes = { 91 | data: PropTypes.any, 92 | }; 93 | -------------------------------------------------------------------------------- /frontend/components/AdminPage/SideBar/SideBarLink.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useRouter } from "next/router"; 4 | 5 | export default function SideBarLink(props) { 6 | const { needRegistry, userHasRegistry, title, href } = props; 7 | 8 | const { pathname, push } = useRouter(); 9 | 10 | const isActive = pathname === href; 11 | const isDisabled = needRegistry !== userHasRegistry; 12 | 13 | const handleClick = e => { 14 | e.preventDefault(); 15 | !isDisabled && push(href); 16 | }; 17 | 18 | return ( 19 |
  • 20 | 21 | {title} 22 | 23 | 45 |
  • 46 | ); 47 | } 48 | 49 | SideBarLink.propTypes = { 50 | title: PropTypes.string.isRequired, 51 | href: PropTypes.string.isRequired, 52 | needRegistry: PropTypes.bool.isRequired, 53 | userHasRegistry: PropTypes.bool.isRequired, 54 | }; 55 | -------------------------------------------------------------------------------- /frontend/components/AdminPage/SideBar/SideBarLinks.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import SideBarLink from "./SideBarLink"; 4 | 5 | const links = [ 6 | { href: "/create", title: "Create Registry", needRegistry: false }, 7 | { href: "/admin", title: "Edit Registry", needRegistry: true }, 8 | { href: "/admin/gifts", title: "Manage Gifts", needRegistry: true }, 9 | { href: "/admin/gifts/create", title: "Add Gift", needRegistry: true }, 10 | { 11 | href: "/admin/gifts/purchases", 12 | title: "View Purchases", 13 | needRegistry: true, 14 | }, 15 | ]; 16 | 17 | export default function SideBarLinks({ userHasRegistry, registryUrl }) { 18 | return ( 19 |
      20 | 26 | {links.map(link => ( 27 | 32 | ))} 33 | 34 | 41 |
    42 | ); 43 | } 44 | 45 | SideBarLinks.propTypes = { 46 | userHasRegistry: PropTypes.bool.isRequired, 47 | registryUrl: PropTypes.string, 48 | }; 49 | -------------------------------------------------------------------------------- /frontend/components/AdminPage/SideBar/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SideBar"; 2 | -------------------------------------------------------------------------------- /frontend/components/AdminPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./AdminPage"; 2 | -------------------------------------------------------------------------------- /frontend/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const defaultBgColor = "bg-purple-500 hover:bg-purple-400"; 5 | 6 | const getClassName = (color = defaultBgColor) => ` 7 | inline-block 8 | shadow 9 | ${color} 10 | focus:shadow-outline 11 | focus:outline-none 12 | text-white 13 | font-bold 14 | py-2 15 | px-4 16 | rounded 17 | `; 18 | 19 | export const styles = getClassName(); 20 | 21 | const Button = ({ type, onClick, children, bgColor, addStyles }) => { 22 | const color = bgColor || defaultBgColor; 23 | const className = getClassName(color); 24 | return ( 25 | 32 | ); 33 | }; 34 | 35 | Button.propTypes = { 36 | type: PropTypes.oneOf(["submit", "button"]), 37 | children: PropTypes.string.isRequired, 38 | onClick: PropTypes.func, 39 | addStyles: PropTypes.string, 40 | bgColor: PropTypes.string, 41 | }; 42 | 43 | Button.defaultProps = { 44 | type: "button", 45 | onClick: () => {}, 46 | bgColor: "", 47 | addStyles: "", 48 | }; 49 | 50 | export default Button; 51 | -------------------------------------------------------------------------------- /frontend/components/DocsAPI/DocsAPI.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NavBar from "../NavBar"; 3 | import Button from "../Button"; 4 | import Header from "../Header"; 5 | import Endpoints from "./Endpoints"; 6 | import { docs, baseGHURL } from "./data"; 7 | import { getAllEndpoints, filterEndpoints } from "./utils"; 8 | 9 | const allEndpoints = getAllEndpoints(docs.routes); 10 | 11 | export default function DocsApi() { 12 | const [searchInput, setSearchInput] = React.useState(""); 13 | 14 | const filteredEndpoints = filterEndpoints(allEndpoints, searchInput); 15 | 16 | return ( 17 | <> 18 |
    19 | 20 |
    21 |
    22 |

    {docs.heading}

    23 |

    {docs.subheading}

    24 |

    25 | {docs.description} 26 |

    27 |
    28 |
    29 | 30 | setSearchInput(e.target.value)} 35 | className="block my-2 mx-auto p-3 w-64 text-center bg-gray-200 rounded-lg shadow " 36 | /> 37 | 38 |
    39 | {searchInput ? ( 40 | <> 41 |

    42 | Search Results 43 |

    44 | {!filteredEndpoints.length ? ( 45 | <> 46 |

    47 | Sorry, nothing matches that search. 48 |

    49 | 56 | 57 | ) : ( 58 | 59 | )} 60 | 61 | ) : ( 62 | docs.routes.map(({ header, referenceURLs, endpoints }) => ( 63 |
    64 |

    {header}

    65 |

    Code References

    66 | 67 | Prefer reading code? Use these links to view the code 68 | 69 | 70 |
      71 | {Object.keys(referenceURLs).map(url => ( 72 |
    • 73 | 79 | {url} 80 | 81 |
    • 82 | ))} 83 |
    84 | 85 |
    86 |

    Routes

    87 | 88 | View all the {header.toLowerCase()} routes below 89 | 90 | 91 |
    92 |
    93 | )) 94 | )} 95 |
    96 |
    97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /frontend/components/DocsAPI/Endpoints.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; 4 | import TextContainer from "./TextContainer"; 5 | import { getMethodColor } from "./utils"; 6 | 7 | const Endpoints = ({ endpoints }) => 8 | endpoints.map( 9 | ({ method, url, description, body, protectedAs, responses }, i) => { 10 | const color = getMethodColor(method); 11 | return ( 12 |
    16 |
    17 | {method} - 18 | {url} 19 |
    20 | 21 | 22 |

    {description}

    23 |
    24 | 25 | 26 |

    Route is protected for: {protectedAs}

    27 |
    28 | 29 | 30 |

    Success: {responses.success}

    31 |

    Failed: {responses.fail}

    32 |
    33 | 34 | 35 | 44 | {body} 45 | 46 | 47 |
    48 | ); 49 | } 50 | ); 51 | 52 | Endpoints.propTypes = { 53 | endpoints: PropTypes.array, 54 | }; 55 | 56 | export default Endpoints; 57 | -------------------------------------------------------------------------------- /frontend/components/DocsAPI/TextContainer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const TextContainer = ({ children, title }) => ( 5 |
    6 | {title} 7 |
    {children}
    8 |
    9 | ); 10 | 11 | TextContainer.propTypes = { 12 | children: PropTypes.oneOfType([ 13 | PropTypes.element, 14 | PropTypes.arrayOf(PropTypes.element), 15 | ]), 16 | title: PropTypes.string, 17 | }; 18 | 19 | export default TextContainer; 20 | -------------------------------------------------------------------------------- /frontend/components/DocsAPI/data.js: -------------------------------------------------------------------------------- 1 | export const baseGHURL = `https://github.com/chingu-voyages/v16-bears-team-04/blob/master/backend/`; 2 | 3 | // body objects are extracted like this, so indentations ... 4 | // ... are accurate while highlighting the code 5 | const createRegistryBody = `body = { 6 | title: String, 7 | description: String, 8 | p1FullName: String, 9 | p2FullName: String, 10 | email: String, 11 | customUrl: String, // no spaces 12 | items: [ String ], 13 | coverImage: String, 14 | userId: String, 15 | tyMessage: String, // optional 16 | weddingDate: Date.now(), // optional 17 | phoneNumber: Number, // optional 18 | }`; 19 | 20 | const updateRegistryBody = `body = { 21 | title: String, // optional 22 | description: String, // optional 23 | p1FullName: String, // optional 24 | p2FullName: String, // optional 25 | email: String, // optional 26 | customUrl: String, // optional 27 | coverImage: String, // optional 28 | tyMessage: String, // optional 29 | weddingDate: Date.now(), // optional 30 | phoneNumber: Number, // optional 31 | }`; 32 | 33 | const madePurchaseBody = `body = { 34 | name: String, // optional 35 | email: String, // optional 36 | message: String, // optional 37 | pricePaid: Number, 38 | }`; 39 | 40 | const updateItemBody = `body = { 41 | name: String, // optional 42 | description: String, // optional 43 | price: Number, // optional 44 | link: String, // optional 45 | image: String, // optional 46 | isReserved: String, // optional 47 | reservedOn: Date.now(), // optional 48 | }`; 49 | 50 | const createItemBody = `body = { 51 | name: String, 52 | description: String, 53 | price: Number, 54 | link: String, // optional 55 | image: String, // optional 56 | }`; 57 | 58 | const deleteMultipleItemsBody = `body = { 59 | arrayOfIds: [ String ] 60 | }`; 61 | 62 | export const docs = { 63 | heading: "Developer Documentation", 64 | subheading: "Wedding Registry API Endpoint Guide", 65 | description: `Useful information to help developers understand and use our backend API`, 66 | author: { name: "Daniel Strong", link: "https://github.com/dastrong" }, 67 | routes: [ 68 | { 69 | header: "Registry", 70 | referenceURLs: { 71 | Model: "src/models/Registry/registry-model.ts", 72 | Routes: "src/routes/Registry/registry-routes.ts", 73 | Controllers: "src/routes/Registry/registry-controllers.ts", 74 | Examples: "src/models/Registry/registry-examples.ts", 75 | Typescript: "src/models/Registry/registry-types.ts", 76 | }, 77 | endpoints: [ 78 | { 79 | method: "GET", 80 | url: "api/registry", 81 | description: "Get all/every registry", 82 | body: "No body needed", 83 | protectedAs: "no one", 84 | responses: { 85 | success: `An array of registry objects with their items array populated`, 86 | fail: "{ message: 'A detailed error message here' }", 87 | }, 88 | }, 89 | { 90 | method: "GET", 91 | url: "api/registry/admin", 92 | description: "Get the registry of the current logged in user", 93 | body: "No body needed", 94 | protectedAs: "registry owner or admin", 95 | responses: { 96 | success: `A registry object`, 97 | fail: "{ message: 'A detailed error message here' }", 98 | }, 99 | }, 100 | { 101 | method: "POST", 102 | url: "api/registry", 103 | description: "Create a single registry", 104 | body: createRegistryBody, 105 | protectedAs: "paid users", 106 | responses: { 107 | success: `An array of registry objects with their items array populated`, 108 | fail: "{ message: 'A detailed error message here' }", 109 | }, 110 | }, 111 | { 112 | method: "GET", 113 | url: "api/registry/:customUrl", 114 | description: "Get a single registry by it's customUrl", 115 | body: "No body needed", 116 | protectedAs: "no one", 117 | responses: { 118 | success: `A registry object with its items array populated`, 119 | fail: "{ message: 'A detailed error message here' }", 120 | }, 121 | }, 122 | { 123 | method: "PUT", 124 | url: "api/registry/:registryId", 125 | description: "Update a single registry", 126 | body: updateRegistryBody, 127 | protectedAs: "registry owners or admins", 128 | responses: { 129 | success: `An array of registry objects with their items array populated`, 130 | fail: "{ message: 'A detailed error message here' }", 131 | }, 132 | }, 133 | { 134 | method: "DELETE", 135 | url: "api/registry/:registryId", 136 | description: "Delete a single registry and all it's items", 137 | body: "No body needed", 138 | protectedAs: "registry owners or admins", 139 | responses: { 140 | success: `{ message: 'Deleted registry and num/num items }`, 141 | fail: "{ message: 'A detailed error message here' }", 142 | }, 143 | }, 144 | ], 145 | }, 146 | { 147 | header: "Items", 148 | referenceURLs: { 149 | Model: "src/models/Items/items-model.ts", 150 | Routes: "src/routes/Items/items-routes.ts", 151 | Controllers: "src/routes/Items/items-controllers.ts", 152 | Examples: "src/models/Items/items-examples.ts", 153 | Typescript: "src/models/Items/items-types.ts", 154 | }, 155 | endpoints: [ 156 | { 157 | method: "GET", 158 | url: "api/item/all", 159 | description: "Get all/every item", 160 | body: "No body needed", 161 | protectedAs: "no one", 162 | responses: { 163 | success: `An array of item objects`, 164 | fail: "{ message: 'A detailed error message here' }", 165 | }, 166 | }, 167 | { 168 | method: "GET", 169 | url: "api/item/registry/:registryId", 170 | description: "Get all item's for the Registry", 171 | body: "No body needed", 172 | protectedAs: "registry owners or admins", 173 | responses: { 174 | success: `An array of item objects`, 175 | fail: "{ message: 'A detailed error message here' }", 176 | }, 177 | }, 178 | { 179 | method: "GET", 180 | url: "api/item/:itemId", 181 | description: "Get a single item by it's id", 182 | body: "No body needed", 183 | protectedAs: "no one", 184 | responses: { 185 | success: `An items object`, 186 | fail: "{ message: 'A detailed error message here' }", 187 | }, 188 | }, 189 | { 190 | method: "POST", 191 | url: "api/item/:itemId", 192 | description: "Update item on purchase and save purchaser's info", 193 | body: madePurchaseBody, 194 | protectedAs: "no one", 195 | responses: { 196 | success: `An updated item object`, 197 | fail: "{ message: 'A detailed error message here' }", 198 | }, 199 | }, 200 | { 201 | method: "PUT", 202 | url: "api/item/:itemId/registry/:registryId", 203 | description: "Update one item's details", 204 | body: updateItemBody, 205 | protectedAs: "registry owners or admins", 206 | responses: { 207 | success: `An updated item object`, 208 | fail: "{ message: 'A detailed error message here' }", 209 | }, 210 | }, 211 | { 212 | method: "DELETE", 213 | url: "api/item/:itemId/registry/:registryId", 214 | description: `Delete one item and remove it from that registry items array`, 215 | body: "No body needed", 216 | protectedAs: "registry owners or admins", 217 | responses: { 218 | success: `The deleted item object`, 219 | fail: "{ message: 'A detailed error message here' }", 220 | }, 221 | }, 222 | { 223 | method: "POST", 224 | url: "api/item/registry/:registryId", 225 | description: `Create one item and add it to it's registry items array`, 226 | body: createItemBody, 227 | protectedAs: "paid users", 228 | responses: { 229 | success: `The new item object`, 230 | fail: "{ message: 'A detailed error message here' }", 231 | }, 232 | }, 233 | { 234 | method: "DELETE", 235 | url: "api/item/registry/:registryId", 236 | description: `Delete multiple items and remove them from that registry items array`, 237 | body: deleteMultipleItemsBody, 238 | protectedAs: "registry owners or admins", 239 | responses: { 240 | success: `{ message: 'Updated registry items and deleted num/num items' }`, 241 | fail: "{ message: 'A detailed error message here' }", 242 | }, 243 | }, 244 | ], 245 | }, 246 | ], 247 | }; 248 | -------------------------------------------------------------------------------- /frontend/components/DocsAPI/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./DocsAPI"; 2 | -------------------------------------------------------------------------------- /frontend/components/DocsAPI/utils.js: -------------------------------------------------------------------------------- 1 | export const getAllEndpoints = routes => { 2 | return routes.reduce((acc, cVal) => [...acc, ...cVal.endpoints], []); 3 | }; 4 | 5 | export const filterEndpoints = (allEndpoints, searchInput) => { 6 | return allEndpoints.filter( 7 | ({ description, method, protectedAs, responses }) => { 8 | const wordsToSearch = `${description} ${method} ${protectedAs} ${responses.success}` 9 | .toLowerCase() 10 | .split(" "); 11 | const splitSearchInput = searchInput.toLowerCase().split(" "); 12 | return splitSearchInput.every(val => 13 | wordsToSearch.some(desc => desc.includes(val)) 14 | ); 15 | } 16 | ); 17 | }; 18 | 19 | export const getMethodColor = method => { 20 | switch (method) { 21 | case "GET": 22 | return "text-green-700"; 23 | case "POST": 24 | return "text-teal-700"; 25 | case "PUT": 26 | return "text-purple-700"; 27 | case "DELETE": 28 | return "text-yellow-700"; 29 | default: 30 | return "text-black"; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import GithubIcon from "@iconscout/react-unicons/icons/uil-github"; 4 | import DocumentIcon from "@iconscout/react-unicons/icons/uil-document"; 5 | import { GiftSVG } from "./LandingPage/svgs"; 6 | 7 | export default function Footer() { 8 | return ( 9 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /frontend/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Head from "next/head"; 4 | 5 | const Header = ({ title = "Home" }) => ( 6 | 7 | {title} | Chingu Registry 8 | 9 | 10 | ); 11 | 12 | Header.propTypes = { 13 | title: PropTypes.string.isRequired, 14 | }; 15 | 16 | export default Header; 17 | -------------------------------------------------------------------------------- /frontend/components/InputText.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { formErrorType } from "../types"; 4 | 5 | const convertNameToId = string => 6 | string.replace(/([A-Z])/g, "-$1").toLowerCase(); 7 | 8 | const WrappedComponent = React.forwardRef(function TextInput( 9 | { id, type, error, min, children }, 10 | ref 11 | ) { 12 | const label = convertNameToId(id); 13 | return ( 14 |
    21 |
    22 | 36 |
    37 |
    38 | 60 | {error && ( 61 |
    {error.message}
    62 | )} 63 |
    64 |
    65 | ); 66 | }); 67 | 68 | WrappedComponent.propTypes = { 69 | id: PropTypes.string.isRequired, 70 | type: PropTypes.string, 71 | children: PropTypes.string.isRequired, 72 | error: formErrorType, 73 | min: PropTypes.string, 74 | }; 75 | 76 | WrappedComponent.defaultProps = { 77 | type: "text", 78 | }; 79 | 80 | export default WrappedComponent; 81 | -------------------------------------------------------------------------------- /frontend/components/Item.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import Modal from "./Modal"; 4 | import { itemType } from "../types"; 5 | import PurchaseItem from "./PurchaseItem"; 6 | 7 | const classItem = ` 8 | group 9 | w-full 10 | max-w-sm 11 | sm:max-w-md 12 | sm:w-1/2 13 | md:w-1/3 14 | lg:w-1/4 15 | xl:w-1/6 16 | mb-4 17 | p-2 18 | transform 19 | hover:scale-105 20 | transition-transform 21 | duration-100 22 | cursor-pointer 23 | `; 24 | 25 | export default function Item(props) { 26 | const { name, price, image, totalPurchased, swrKey } = props; 27 | 28 | const [isPurchaseOpen, setIsPurchaseOpen] = useState(false); 29 | 30 | const priceLeft = price - totalPurchased; 31 | const isBought = priceLeft < 1; 32 | 33 | // don't open the modal if the item is already purchased 34 | const handleOpen = () => setIsPurchaseOpen(!isBought); 35 | const handleClose = () => setIsPurchaseOpen(false); 36 | 37 | return ( 38 | <> 39 |
    40 |
    41 | {name} 46 | 47 | {/* Item info */} 48 |
    49 |
    {name}
    50 |

    51 | {isBought ? ( 52 | "Bought" 53 | ) : ( 54 | <> 55 | Remaining Goal: $ 56 | {priceLeft} 57 | 58 | )} 59 |

    60 |
    61 |
    62 | 63 | {/* Partially see through hovered cover */} 64 |
    65 | {isBought ? "Goal price reached" : "Click to purchase"} 66 |
    67 | 68 | {/* Colored bottom border effect */} 69 |
    74 |
    75 | 76 | 77 | 83 | 84 | 85 | ); 86 | } 87 | 88 | Item.propTypes = { 89 | swrKey: PropTypes.string.isRequired, 90 | ...itemType, 91 | }; 92 | -------------------------------------------------------------------------------- /frontend/components/ItemForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useRouter } from "next/router"; 4 | import { useAuth } from "use-auth0-hooks"; 5 | import { mutate } from "swr"; 6 | import { useForm } from "react-hook-form"; 7 | 8 | import InputText from "./InputText"; 9 | import Button from "./Button"; 10 | import Link from "./Link"; 11 | import { adminFetchIt, AUTH0_API_IDENTIFIER } from "../utils"; 12 | import { useSnacks } from "./Snack"; 13 | 14 | const audience = AUTH0_API_IDENTIFIER; 15 | 16 | export default function ItemForm({ defaultValues = {}, isCreating = false }) { 17 | const method = isCreating ? "POST" : "PUT"; 18 | 19 | const { push } = useRouter(); 20 | const { accessToken } = useAuth({ audience }); 21 | const { register, handleSubmit, errors, reset } = useForm({ 22 | defaultValues, 23 | }); 24 | const { openSnack } = useSnacks(); 25 | 26 | // update and create url structure 27 | // PUT - api/item/:itemId/registry/:registryId 28 | // POST - api/item/ registry/:registryId 29 | 30 | const onSubmit = formData => { 31 | mutate(["/registry/admin", accessToken], async registry => { 32 | // creates a url based on if its we're creating/updating 33 | const url = `/item${isCreating ? "" : `/${defaultValues._id}`}/registry/${ 34 | registry._id 35 | }`; 36 | 37 | try { 38 | const changedItem = await adminFetchIt(url, accessToken, { 39 | method, 40 | body: JSON.stringify(formData), 41 | }); 42 | 43 | const updatedItems = isCreating 44 | ? // adds the new item to the items array 45 | [...registry.items, changedItem] 46 | : // updates the items array with the updated item 47 | registry.items.map(item => 48 | item._id === changedItem._id ? changedItem : item 49 | ); 50 | 51 | // this will reset the form to allow the user to create ... 52 | // ... multiple items or return the user to see their updated gifts 53 | if (isCreating) { 54 | openSnack("Success! Added your gift", "success"); 55 | reset(defaultValues); 56 | } else { 57 | openSnack("Success! Updated your gift", "success"); 58 | push("/admin/gifts"); 59 | } 60 | 61 | return { ...registry, items: updatedItems }; 62 | } catch (err) { 63 | console.log(err); 64 | openSnack( 65 | `Sorry! We couldn't ${isCreating ? "create" : "edit"} that gift`, 66 | "error" 67 | ); 68 | return registry; 69 | } 70 | }); 71 | }; 72 | 73 | return ( 74 |
    75 | 80 | Name 81 | 82 | 88 | Description 89 | 90 | 97 | Price 98 | 99 | 100 | Link 101 | 102 | 103 | Image 104 | 105 | 108 | Cancel 109 |
    110 | ); 111 | } 112 | 113 | ItemForm.propTypes = { 114 | defaultValues: PropTypes.object, 115 | isCreating: PropTypes.bool, 116 | }; 117 | -------------------------------------------------------------------------------- /frontend/components/Items.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Item from "./Item"; 4 | import { itemType } from "../types"; 5 | 6 | function Items({ items, swrKey }) { 7 | return ( 8 |
    9 | {items.map(item => ( 10 | 11 | ))} 12 |
    13 | ); 14 | } 15 | 16 | Items.propTypes = { 17 | items: PropTypes.arrayOf(PropTypes.shape(itemType)), 18 | swrKey: PropTypes.string.isRequired, 19 | }; 20 | 21 | export default Items; 22 | export { itemType }; 23 | -------------------------------------------------------------------------------- /frontend/components/LandingPage/Hero.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GiftWishesSVG } from "./svgs"; 3 | 4 | export default function Hero() { 5 | return ( 6 |
    7 |
    8 |

    9 | The perfect wedding registry 10 |

    11 |

    12 | We keep it simple for you and your guests 13 |

    14 |
    15 | 16 | 17 | 18 | 89 |
    90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /frontend/components/LandingPage/Info.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GiftCardSVG, GiftBoxSVG, GiftSendSVG } from "./svgs"; 3 | 4 | const data = [ 5 | { 6 | svg: , 7 | heading: "Customizable", 8 | subheading: "Layout. Gifts. Edit Anything.", 9 | description: `We make it easy to update the look of your registry, so you feel into control.`, 10 | }, 11 | { 12 | svg: , 13 | heading: "User Friendly", 14 | subheading: "Simply beautiful", 15 | description: `We use our industry best practice to protect you and keep your focus on your special day.`, 16 | }, 17 | { 18 | svg: , 19 | heading: "Ease of Use", 20 | subheading: "Industry best practices", 21 | description: `We make it easy for your friends and family to contribute to the things that you actually want.`, 22 | }, 23 | ]; 24 | 25 | export default function Info() { 26 | return ( 27 |
    28 |
    29 |
    30 |

    31 | What you'll love about Chingu Registry 32 |

    33 |

    34 | It's free and easy to use 35 |

    36 |
    37 |
    38 |
    39 | 40 |
    41 | {data.map(({ svg, heading, subheading, description }, i) => ( 42 |
    48 | {svg} 49 |
    54 |

    {heading}

    55 |

    56 | {subheading} 57 |

    58 |

    {description}

    59 |
    60 |
    61 | ))} 62 |
    63 |
    64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /frontend/components/LandingPage/Landing.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NavBar from "../../components/NavBar"; 3 | import Hero from "./Hero"; 4 | import Info from "./Info"; 5 | import Footer from "../Footer"; 6 | import Header from "../Header"; 7 | 8 | export default function LandingPage() { 9 | return ( 10 |
    11 |
    12 | 13 | 14 | 15 |
    16 | 17 | 18 |
    19 | 20 |
    21 |
    22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /frontend/components/LandingPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Landing"; 2 | -------------------------------------------------------------------------------- /frontend/components/LandingPage/svgs/GiftBoxSVG.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function GiftBoxSVG() { 4 | return ( 5 | 10 | 16 | 17 | 22 | 27 | 28 | 33 | 34 | 39 | 40 | 45 | 46 | 53 | 54 | 55 | 56 | 61 | 62 | 67 | 72 | 76 | 81 | 86 | 87 | 91 | 92 | 97 | 102 | 106 | 111 | 116 | 121 | 126 | 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /frontend/components/LandingPage/svgs/GiftCardSVG.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function GiftCardSVG() { 4 | return ( 5 | 10 | 16 | 17 | 25 | 35 | 45 | 53 | 61 | 69 | 79 | 89 | 99 | 104 | 110 | 120 | 121 | 127 | 132 | 139 | 146 | 151 | 156 | 161 | 166 | 171 | 176 | 181 | 186 | 191 | 196 | 197 | 202 | 206 | 211 | 216 | 221 | 227 | 232 | 239 | 246 | 251 | 258 | 263 | 270 | 275 | 282 | 283 | ); 284 | } 285 | -------------------------------------------------------------------------------- /frontend/components/LandingPage/svgs/GiftSendSVG.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function GiftSendSVG() { 4 | return ( 5 | 10 | 16 | 17 | 18 | 23 | 28 | 33 | 38 | 39 | 40 | 41 | 46 | 47 | 48 | 49 | 50 | 51 | 56 | 57 | 58 | 62 | 67 | 72 | 76 | 80 | 85 | 90 | 91 | 95 | 100 | 105 | 110 | 115 | 120 | 124 | 128 | 133 | 138 | 139 | 140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /frontend/components/LandingPage/svgs/GiftWishesSVG.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function GiftWishes() { 4 | return ( 5 | 10 | 28 | 33 | 38 | 44 | 49 | 55 | 63 | 68 | 74 | 79 | 85 | 93 | 98 | 103 | 108 | 113 | 118 | 119 | 123 | 124 | 129 | 133 | 138 | 143 | 150 | 151 | 155 | 161 | 166 | 171 | 177 | 185 | 189 | 193 | 197 | 201 | 205 | 209 | 213 | 218 | 223 | 224 | ); 225 | } 226 | -------------------------------------------------------------------------------- /frontend/components/LandingPage/svgs/index.js: -------------------------------------------------------------------------------- 1 | import GiftSVG from "./GiftSVG"; 2 | import GiftBoxSVG from "./GiftBoxSVG"; 3 | import GiftCardSVG from "./GiftCardSVG"; 4 | import GiftSendSVG from "./GiftSendSVG"; 5 | import GiftWishesSVG from "./GiftWishesSVG"; 6 | 7 | export { GiftSVG, GiftBoxSVG, GiftCardSVG, GiftSendSVG, GiftWishesSVG }; 8 | -------------------------------------------------------------------------------- /frontend/components/Link.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { string, bool } from "prop-types"; 4 | 5 | import { styles } from "./Button"; 6 | 7 | const StyledLink = props => ( 8 | 9 | {props.children} 10 | 11 | ); 12 | 13 | StyledLink.propTypes = { 14 | href: string.isRequired, 15 | as: string, 16 | passHref: bool, 17 | prefetch: bool, 18 | replace: bool, 19 | scroll: bool, 20 | children: string.isRequired, 21 | }; 22 | 23 | export default StyledLink; 24 | -------------------------------------------------------------------------------- /frontend/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export default function Loader({ 5 | size = 60, 6 | text = "Loading...", 7 | addStyles = "", 8 | children, 9 | }) { 10 | const baseStyles = "flex flex-col justify-center items-center text-lg my-4"; 11 | const className = `${baseStyles} ${addStyles}`; 12 | 13 | return ( 14 |
    15 | 16 | 17 | 18 | {text} 19 | {children} 20 | 21 | 45 |
    46 | ); 47 | } 48 | 49 | Loader.propTypes = { 50 | size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 51 | text: PropTypes.string, 52 | addStyles: PropTypes.string, 53 | children: PropTypes.node, 54 | }; 55 | -------------------------------------------------------------------------------- /frontend/components/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { createRef, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { CSSTransition } from "react-transition-group"; 4 | import Portal from "./Portal"; 5 | 6 | const Modal = props => { 7 | return ( 8 | <> 9 | 10 | 16 |
    22 | 23 |
    24 |
    25 |
    26 | 48 | 49 | ); 50 | }; 51 | 52 | // Seperated out into it's own component because refs was 53 | // acting funny to child elements of the CSSTransition component 54 | const InnerModal = ({ children, handleClose }) => { 55 | useEffect(() => { 56 | function keyListener(e) { 57 | const listener = keyListenersMap.get(e.keyCode); 58 | return listener && listener(e); 59 | } 60 | 61 | document.addEventListener("keydown", keyListener); 62 | 63 | return () => document.removeEventListener("keydown", keyListener); 64 | }, []); 65 | 66 | const modalRef = createRef(); 67 | 68 | // To make the modal nice and accessible, we want to trap the 69 | // focusable elements to only elements from inside the modal 70 | const handleTabKey = e => { 71 | const focusableModalElements = modalRef.current.querySelectorAll( 72 | 'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select' 73 | ); 74 | const firstElement = focusableModalElements[0]; 75 | const lastElement = 76 | focusableModalElements[focusableModalElements.length - 1]; 77 | 78 | if (!e.shiftKey && document.activeElement === lastElement) { 79 | firstElement.focus(); 80 | return e.preventDefault(); 81 | } 82 | 83 | if (e.shiftKey && document.activeElement === firstElement) { 84 | lastElement.focus(); 85 | e.preventDefault(); 86 | } 87 | }; 88 | 89 | const keyListenersMap = new Map([ 90 | [27, handleClose], 91 | [9, handleTabKey], 92 | ]); 93 | 94 | return ( 95 |
    { 99 | e.preventDefault(); 100 | e.stopPropagation(); 101 | }} 102 | > 103 | {children} 104 | 105 | 110 |
    111 | ); 112 | }; 113 | 114 | const modalProps = { 115 | isOpen: PropTypes.bool.isRequired, 116 | handleClose: PropTypes.func.isRequired, 117 | children: PropTypes.node.isRequired, 118 | }; 119 | 120 | Modal.propTypes = modalProps; 121 | InnerModal.propTypes = modalProps; 122 | 123 | export default Modal; 124 | -------------------------------------------------------------------------------- /frontend/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import Link from "next/link"; 4 | import { useRouter } from "next/router"; 5 | import { useAuth } from "use-auth0-hooks"; 6 | import GiftIcon from "@iconscout/react-unicons/icons/uil-gift"; 7 | import BarsIcon from "@iconscout/react-unicons/icons/uil-bars"; 8 | import TimesIcon from "@iconscout/react-unicons/icons/uil-times"; 9 | import { REDIRECTURI } from "../utils"; 10 | 11 | const StyledItem = ({ children }) => ( 12 |
  • 13 | {children} 14 |
  • 15 | ); 16 | 17 | StyledItem.propTypes = { 18 | children: PropTypes.node, 19 | }; 20 | 21 | const StyledLink = ({ text, ...props }) => ( 22 | 23 | 24 | {text} 25 | 26 | 27 | ); 28 | 29 | StyledLink.propTypes = { 30 | text: PropTypes.string, 31 | href: PropTypes.string, 32 | }; 33 | 34 | export default function NavBar() { 35 | const [isMenuOpen, setIsOpenMenu] = useState(false); 36 | const { pathname, query } = useRouter(); 37 | const { isAuthenticated, isLoading, login, logout } = useAuth(); 38 | 39 | const menuIconProps = { 40 | size: "45", 41 | color: "#9f7aea", 42 | className: `md:hidden cursor-pointer p-1 rounded-full hover:bg-white transition-all duration-150`, 43 | onClick: () => setIsOpenMenu(state => !state), 44 | }; 45 | 46 | return ( 47 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /frontend/components/Portal.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, useState } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | const Portal = ({ children, selector }) => { 5 | const ref = useRef(); 6 | const [mounted, setMounted] = useState(false); 7 | 8 | useEffect(() => { 9 | ref.current = document.querySelector(selector); 10 | setMounted(true); 11 | }, [selector]); 12 | 13 | return mounted ? createPortal(children, ref.current) : null; 14 | }; 15 | 16 | export default Portal; 17 | -------------------------------------------------------------------------------- /frontend/components/PurchaseItem.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useForm } from "react-hook-form"; 4 | import { mutate } from "swr"; 5 | import InputText from "./InputText"; 6 | import Button from "./Button"; 7 | import { useSnacks } from "./Snack"; 8 | import { itemType } from "../types"; 9 | import { fetchIt } from "../utils"; 10 | 11 | const PurchaseItem = ({ 12 | _id, 13 | name, 14 | description, 15 | link, 16 | image, 17 | handleClose, 18 | priceLeft, 19 | swrKey, 20 | }) => { 21 | const { register, handleSubmit, errors } = useForm(); 22 | const { openSnack } = useSnacks(); 23 | const formRef = useRef(); 24 | 25 | const onFormSubmit = async formData => { 26 | mutate(swrKey, async registry => { 27 | try { 28 | const updatedItem = await fetchIt(`/item/${_id}`, { 29 | method: "POST", 30 | body: JSON.stringify(formData), 31 | }); 32 | const updatedItems = registry.items.map(item => 33 | item._id === _id ? updatedItem : item 34 | ); 35 | openSnack("Success! Processed your payment.", "success"); 36 | handleClose(); 37 | return { ...registry, items: updatedItems }; 38 | } catch (err) { 39 | console.log(err); 40 | openSnack("Sorry! Couldn't process your payment", "error"); 41 | handleClose(); 42 | return registry; 43 | } 44 | }); 45 | }; 46 | 47 | // For some reason, submitting the form inside the modal doesn't work. 48 | // I think it has something to do with this form being inside a 49 | // react portal, meaning it's injected when the modal is opened. 50 | // This function fires the normal form onSubmit event. 51 | const onSubmit = e => { 52 | e.preventDefault(); 53 | formRef.current.dispatchEvent(new Event("submit")); 54 | }; 55 | 56 | return ( 57 |
    58 |
    59 | {`${name} 64 |
    65 | 66 |
    67 |
    68 |

    {name}

    69 |
    70 | Remaining Goal: ${priceLeft} 71 |
    72 |
    73 | 85 |
    86 | 87 |
    88 | {description} 89 |
    90 | 91 |
    92 |
    93 |

    Thank you for choosing to gift this!

    94 |

    95 | Please fill out the details below to gift this gift. 96 |

    97 | 98 | Note: 99 | only the amount is required. Feel free to gift anonymously. 100 | 101 |
    102 | 103 |
    104 | 105 | Name 106 | 107 | 113 | Email 114 | 115 | 121 | Message 122 | 123 | 139 | Amount ($) 140 | 141 |
    142 | 148 | 155 |
    156 |
    157 |
    158 |
    159 | ); 160 | }; 161 | 162 | PurchaseItem.propTypes = { 163 | ...itemType, 164 | handleClose: PropTypes.func.isRequired, 165 | priceLeft: PropTypes.number.isRequired, 166 | swrKey: PropTypes.string.isRequired, 167 | }; 168 | 169 | export default PurchaseItem; 170 | -------------------------------------------------------------------------------- /frontend/components/Purchases.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { itemType } from "../types"; 4 | import colors from "../css/colors"; 5 | import { getTotalPricePaid } from "../utils"; 6 | 7 | export default function Purchases({ items }) { 8 | return ( 9 |
    10 | {items.map(({ name, description, image, price, purchasers }) => { 11 | // cumulative price paid for this item 12 | const totalPricePaid = getTotalPricePaid(purchasers); 13 | 14 | return ( 15 |
    19 |
    20 |
    21 |

    {name}

    22 |

    {description}

    23 |
    24 |

    25 | Purchase Status ($) 26 |

    27 |

    28 | {totalPricePaid} 29 | / 30 | {price} 31 |

    32 |
    33 |
    34 |
    35 | {name.slice(0, 40 |
    41 |
    42 | 43 |
      44 | {purchasers.map((purchaser, i) => ( 45 |
    • 49 | 53 | 57 | 58 | 59 | 60 |
    • 61 | ))} 62 |
    63 |
    64 | ); 65 | })} 66 | 71 |
    72 | ); 73 | } 74 | const UserInfo = ({ title, text }) => ( 75 |

    76 | {`${title}: `} 77 | 78 | {text || `Sorry, no ${title.toLowerCase()} provided`} 79 | 80 |

    81 | ); 82 | 83 | UserInfo.propTypes = { 84 | title: PropTypes.string.isRequired, 85 | text: PropTypes.string, 86 | }; 87 | 88 | Purchases.propTypes = { 89 | items: PropTypes.arrayOf(PropTypes.shape(itemType)), 90 | }; 91 | -------------------------------------------------------------------------------- /frontend/components/RegistryForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useRouter } from "next/router"; 4 | import { mutate } from "swr"; 5 | import { adminFetchIt, AUTH0_API_IDENTIFIER } from "../utils"; 6 | import { useForm } from "react-hook-form"; 7 | import { useAuth } from "use-auth0-hooks"; 8 | import InputText from "./InputText"; 9 | import Button from "./Button"; 10 | import { useSnacks } from "./Snack"; 11 | 12 | export default function RegistryForm({ 13 | defaultValues = {}, 14 | isCreating = false, 15 | }) { 16 | const { push } = useRouter(); 17 | const { accessToken, user } = useAuth({ audience: AUTH0_API_IDENTIFIER }); 18 | const { openSnack } = useSnacks(); 19 | 20 | const method = isCreating ? "POST" : "PUT"; 21 | const email = isCreating ? user.email : defaultValues.email || ""; 22 | const weddingDate = defaultValues.weddingDate 23 | ? defaultValues.weddingDate.substring(0, 10) 24 | : ""; 25 | 26 | const { register, handleSubmit, errors } = useForm({ 27 | defaultValues: { email, ...defaultValues, weddingDate }, 28 | }); 29 | 30 | const onSubmit = async formData => { 31 | await mutate( 32 | ["/registry/admin", accessToken], 33 | // the registry below is the cached version from AdminPage 34 | async registry => { 35 | // gets the correct api url 36 | const url = `/registry${!isCreating ? `/${registry._id}` : ""}`; 37 | 38 | try { 39 | const changedRegistry = await adminFetchIt(url, accessToken, { 40 | method, 41 | body: JSON.stringify(formData), 42 | }); 43 | const modifiedRegistry = { 44 | ...changedRegistry, 45 | // we don't want any array of the item STRINGs, we ... 46 | // ... want the item OBJECTs still 47 | items: isCreating ? [] : registry.items, 48 | }; 49 | 50 | openSnack( 51 | `Success! ${isCreating ? "Created" : "Updated"} your registry`, 52 | "success" 53 | ); 54 | 55 | return modifiedRegistry; 56 | } catch (err) { 57 | console.log(err); 58 | openSnack( 59 | `Sorry! We couldn't ${ 60 | isCreating ? "create" : "edit" 61 | } that registry`, 62 | "error" 63 | ); 64 | return registry; 65 | } 66 | } 67 | ); 68 | 69 | if (isCreating) { 70 | push("/admin"); 71 | } 72 | }; 73 | 74 | return ( 75 |
    76 | 81 | Title 82 | 83 | 89 | Description 90 | 91 | 96 | Partner 1 Full Name 97 | 98 | 103 | Partner 2 Full Name 104 | 105 | 111 | Email 112 | 113 | 119 | Phone Number 120 | 121 | 127 | Wedding Date 128 | 129 | 130 | Thank You Message 131 | 132 | 140 | Custom URL 141 | 142 | 143 | Cover Image 144 | 145 | 146 |
    147 | ); 148 | } 149 | 150 | RegistryForm.propTypes = { 151 | defaultValues: PropTypes.object, 152 | isCreating: PropTypes.bool, 153 | }; 154 | -------------------------------------------------------------------------------- /frontend/components/Snack.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useMemo, 4 | useContext, 5 | createContext, 6 | useCallback, 7 | useEffect, 8 | } from "react"; 9 | import PropTypes from "prop-types"; 10 | import CloseIcon from "@iconscout/react-unicons/icons/uil-times"; 11 | 12 | function getColorString(type) { 13 | const color = getColor(type); 14 | return `bg-${color}-500 border-2 border-${color}-900`; 15 | } 16 | 17 | function getColor(type) { 18 | if (type === "success") return `green`; 19 | if (type === "error") return `red`; 20 | if (type === "info") return `blue`; 21 | return `blue`; 22 | } 23 | 24 | const AdminPageContext = createContext({}); 25 | 26 | export const useSnacks = () => useContext(AdminPageContext); 27 | 28 | export default function SnackProvider({ children }) { 29 | const [isSnackOpen, setIsSnackOpen] = useState(false); 30 | const [snack, setSnack] = useState(""); 31 | const [type, setType] = useState("info"); 32 | 33 | const openSnack = useCallback((snack, type) => { 34 | setSnack(snack); 35 | setType(type); 36 | setIsSnackOpen(true); 37 | }); 38 | 39 | const closeSnack = useCallback(() => { 40 | setIsSnackOpen(false); 41 | }); 42 | 43 | // memotize handlers to avoid unnecessary rerenders 44 | const handlers = useMemo(() => ({ openSnack, closeSnack }), [snack, type]); 45 | 46 | useEffect(() => { 47 | let id; 48 | if (isSnackOpen) { 49 | id = setTimeout(() => closeSnack(), 8000); 50 | } 51 | return () => { 52 | clearTimeout(id); 53 | }; 54 | }, [isSnackOpen]); 55 | 56 | const color = getColorString(type); 57 | const className = `${color} pl-4 pr-6 py-4 m-3 z-50 rounded-lg text-white text-lg`; 58 | 59 | return ( 60 | 61 | {children} 62 | 63 |
    64 | {snack} 65 | 70 |
    71 | 72 | 84 |
    85 | ); 86 | } 87 | 88 | SnackProvider.propTypes = { 89 | children: PropTypes.node.isRequired, 90 | }; 91 | -------------------------------------------------------------------------------- /frontend/css/colors.js: -------------------------------------------------------------------------------- 1 | export default { 2 | backgroundPrimary: "#fff", 3 | backgroundSecondary: "#f5f5f5", 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | // Tell webpack to compile the "bar" package, necessary if you're using the export statement for example 2 | // https://www.npmjs.com/package/next-transpile-modules 3 | const withTM = require("next-transpile-modules")(["@iconscout/react-unicons"]); 4 | 5 | module.exports = withTM(); 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "pretest": "./node_modules/.bin/eslint --ignore-path .gitignore . --fix" 10 | }, 11 | "dependencies": { 12 | "@fullhuman/postcss-purgecss": "^2.0.6", 13 | "@iconscout/react-unicons": "^0.0.1", 14 | "autoprefixer": "^9.7.4", 15 | "isomorphic-fetch": "^2.2.1", 16 | "next": "9.2.1", 17 | "next-transpile-modules": "^3.0.2", 18 | "postcss-import": "^12.0.1", 19 | "react": "16.12.0", 20 | "react-dom": "16.12.0", 21 | "react-hook-form": "^4.9.6", 22 | "react-syntax-highlighter": "^12.2.1", 23 | "react-transition-group": "^4.3.0", 24 | "react-use-auth": "^0.5.3", 25 | "swr": "^0.1.18", 26 | "tailwindcss": "^1.2.0", 27 | "use-auth0-hooks": "^0.7.0" 28 | }, 29 | "devDependencies": { 30 | "dotenv": "^8.2.0", 31 | "eslint": "^6.8.0", 32 | "eslint-config-prettier": "^6.10.0", 33 | "eslint-plugin-prettier": "^3.1.2", 34 | "eslint-plugin-react": "^7.18.3", 35 | "husky": "^4.2.2", 36 | "lint-staged": "^10.0.7", 37 | "prettier": "1.19.1", 38 | "prop-types": "^15.7.2" 39 | }, 40 | "husky": { 41 | "hooks": { 42 | "pre-commit": "lint-staged" 43 | } 44 | }, 45 | "lint-staged": { 46 | "*.js": "eslint --fix" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/pages/[registryUrl].js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import ErrorPage from "next/error"; 4 | import useSWR from "swr"; 5 | import Items from "../components/Items"; 6 | import { fetchIt } from "../utils"; 7 | import Footer from "../components/Footer"; 8 | import { publicRegistryType } from "../types"; 9 | import colors from "../css/colors"; 10 | import Header from "../components/Header"; 11 | 12 | /* 13 | This is the main React component, the HTML that gets returned from this function is what 14 | will show on the browser. 15 | 16 | The "registryUrl" variable is what we call a "prop" in react, and is being passed into the component 17 | like a parameter to a function 18 | */ 19 | export default function RegistryPage({ registryUrl, initialData, error }) { 20 | // this is the key we'll need to mutate when a purchase is made 21 | const swrKey = `/registry/${registryUrl}`; 22 | // pass in the server rendered registry instead of making... 23 | // ... an unnecessary client side call 24 | const { data } = useSWR(swrKey, { initialData }); 25 | 26 | if (error) return ; 27 | 28 | return ( 29 | <> 30 |
    31 |
    32 |
    33 |
    34 |

    {data.title}

    35 |

    {data.description}

    36 |
    37 |
    38 |
    39 | 40 |
    41 |
    42 | 43 |