diff --git a/README.md b/README.md
index 946b715..6a7eb58 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,10 @@
-# voyage-tasks
+# Chingu Collaborate
-Your project's `readme` is as important to success as your code. For
-this reason you should put as much care into its creation and maintenance
-as you would any other component of the application.
+This project aims to streamline and address some of the main issues associated with the primary offering of Chingu, known as Voyages.
-If you are unsure of what should go into the `readme` let this article,
-written by an experienced Chingu, be your starting point -
-[Keys to a well written README](https://tinyurl.com/yk3wubft).
+Firstly, with self-taught developers emerging daily, we wanted to create a platform where Chingus don't have to wait for the next available voyage that may sometimes be months away.
-And before we go there's "one more thing"! Once you decide what to include
-in your `readme` feel free to replace the text we've provided here.
+Secondly, while Chingus may initially be able to commit to a Voyage there may be commitments along the way that lead them to abandoning Voyages before completion. Unfortunately, this can directly impact other Chingus and their experience with Voyages too. For example, teams can quickly go from having 5 members assigned to their voyage, to 2 members. The main problem here is that with 2 members in a voyage, Chingus aren't able to develop their collaborative learning skills that they intended to develop through their Voyage.
+
+For these reasons, we decided to create a platform to help Chingus. Using Chingu Collaborate, Chingus will be able to collborate with other decicated Chingus in the event they miss the deadline to join a Voyage or the next Voyage is months away. Additionally, Voyage teams can leverage Chingu Collaborate to gain help with their Voyages.
-> Own it & Make it your Own!
diff --git a/package-lock.json b/package-lock.json
index 2fd3c39..60d7935 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,14 +12,18 @@
"@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0",
"@reduxjs/toolkit": "^1.8.4",
+ "chakra-react-select": "^4.1.5",
"framer-motion": "^7.2.0",
+ "luxon": "^3.0.3",
"mongodb": "^4.8.1",
"mongoose": "^6.5.2",
"next": "12.2.5",
"next-auth": "^4.10.3",
"react": "18.2.0",
"react-dom": "18.2.0",
- "react-icons": "^4.4.0"
+ "react-icons": "^4.4.0",
+ "react-select": "^5.4.0",
+ "swr": "^1.3.0"
},
"devDependencies": {
"eslint": "8.22.0",
@@ -2023,6 +2027,34 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
+ "node_modules/@types/prop-types": {
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
+ },
+ "node_modules/@types/react": {
+ "version": "18.0.18",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.18.tgz",
+ "integrity": "sha512-6hI08umYs6NaiHFEEGioXnxJ+oEhY3eRz8VCUaudZmGdtvPviCJB8mgaMxaDWAdPSYd4eFavrPk2QIolwbLYrg==",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+ },
"node_modules/@types/webidl-conversions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz",
@@ -2488,6 +2520,25 @@
}
]
},
+ "node_modules/chakra-react-select": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/chakra-react-select/-/chakra-react-select-4.1.5.tgz",
+ "integrity": "sha512-a1HarCQ7aNpJ0T/AFIvvnlXA5iWGLYfeXE07QSga11YGfzbA49Jyx7OQwnhQKthT/jIgmhhbez0gsCrvlgM7Gw==",
+ "dependencies": {
+ "react-select": "^5.4.0"
+ },
+ "peerDependencies": {
+ "@chakra-ui/form-control": "^2.0.0",
+ "@chakra-ui/icon": "^3.0.0",
+ "@chakra-ui/layout": "^2.0.0",
+ "@chakra-ui/menu": "^2.0.0",
+ "@chakra-ui/spinner": "^2.0.0",
+ "@chakra-ui/system": "^2.0.0",
+ "@emotion/react": "^11.8.1",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2691,6 +2742,15 @@
"node": ">=6.0.0"
}
},
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.4.222",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.222.tgz",
@@ -4136,6 +4196,19 @@
"node": ">=10"
}
},
+ "node_modules/luxon": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.3.tgz",
+ "integrity": "sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/memoize-one": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
+ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
+ },
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
@@ -4879,6 +4952,24 @@
}
}
},
+ "node_modules/react-select": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.4.0.tgz",
+ "integrity": "sha512-CjE9RFLUvChd5SdlfG4vqxZd55AZJRrLrHzkQyTYeHlpOztqcgnyftYAolJ0SGsBev6zAs6qFrjm6KU3eo2hzg==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.0",
+ "@emotion/cache": "^11.4.0",
+ "@emotion/react": "^11.8.1",
+ "@types/react-transition-group": "^4.4.0",
+ "memoize-one": "^5.0.0",
+ "prop-types": "^15.6.0",
+ "react-transition-group": "^4.3.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -4901,6 +4992,21 @@
}
}
},
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
"node_modules/redux": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz",
@@ -5300,6 +5406,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/swr": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-1.3.0.tgz",
+ "integrity": "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==",
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -7080,6 +7194,34 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
+ "@types/prop-types": {
+ "version": "15.7.5",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
+ "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
+ },
+ "@types/react": {
+ "version": "18.0.18",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.18.tgz",
+ "integrity": "sha512-6hI08umYs6NaiHFEEGioXnxJ+oEhY3eRz8VCUaudZmGdtvPviCJB8mgaMxaDWAdPSYd4eFavrPk2QIolwbLYrg==",
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/scheduler": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
+ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
+ },
"@types/webidl-conversions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz",
@@ -7378,6 +7520,14 @@
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001378.tgz",
"integrity": "sha512-JVQnfoO7FK7WvU4ZkBRbPjaot4+YqxogSDosHv0Hv5mWpUESmN+UubMU6L/hGz8QlQ2aY5U0vR6MOs6j/CXpNA=="
},
+ "chakra-react-select": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/chakra-react-select/-/chakra-react-select-4.1.5.tgz",
+ "integrity": "sha512-a1HarCQ7aNpJ0T/AFIvvnlXA5iWGLYfeXE07QSga11YGfzbA49Jyx7OQwnhQKthT/jIgmhhbez0gsCrvlgM7Gw==",
+ "requires": {
+ "react-select": "^5.4.0"
+ }
+ },
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -7535,6 +7685,15 @@
"esutils": "^2.0.2"
}
},
+ "dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "requires": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"electron-to-chromium": {
"version": "1.4.222",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.222.tgz",
@@ -8627,6 +8786,16 @@
"yallist": "^4.0.0"
}
},
+ "luxon": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.0.3.tgz",
+ "integrity": "sha512-+EfHWnF+UT7GgTnq5zXg3ldnTKL2zdv7QJgsU5bjjpbH17E3qi/puMhQyJVYuCq+FRkogvB5WB6iVvUr+E4a7w=="
+ },
+ "memoize-one": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
+ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
+ },
"memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
@@ -9131,6 +9300,20 @@
"tslib": "^2.0.0"
}
},
+ "react-select": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.4.0.tgz",
+ "integrity": "sha512-CjE9RFLUvChd5SdlfG4vqxZd55AZJRrLrHzkQyTYeHlpOztqcgnyftYAolJ0SGsBev6zAs6qFrjm6KU3eo2hzg==",
+ "requires": {
+ "@babel/runtime": "^7.12.0",
+ "@emotion/cache": "^11.4.0",
+ "@emotion/react": "^11.8.1",
+ "@types/react-transition-group": "^4.4.0",
+ "memoize-one": "^5.0.0",
+ "prop-types": "^15.6.0",
+ "react-transition-group": "^4.3.0"
+ }
+ },
"react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -9141,6 +9324,17 @@
"tslib": "^2.0.0"
}
},
+ "react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ }
+ },
"redux": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz",
@@ -9415,6 +9609,12 @@
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
},
+ "swr": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-1.3.0.tgz",
+ "integrity": "sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==",
+ "requires": {}
+ },
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
diff --git a/package.json b/package.json
index 12f4c89..9572ab6 100644
--- a/package.json
+++ b/package.json
@@ -13,14 +13,18 @@
"@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0",
"@reduxjs/toolkit": "^1.8.4",
+ "chakra-react-select": "^4.1.5",
"framer-motion": "^7.2.0",
+ "luxon": "^3.0.3",
"mongodb": "^4.8.1",
"mongoose": "^6.5.2",
"next": "12.2.5",
"next-auth": "^4.10.3",
"react": "18.2.0",
"react-dom": "18.2.0",
- "react-icons": "^4.4.0"
+ "react-icons": "^4.4.0",
+ "react-select": "^5.4.0",
+ "swr": "^1.3.0"
},
"devDependencies": {
"eslint": "8.22.0",
diff --git a/pages/_app.js b/pages/_app.js
deleted file mode 100644
index 1e1cec9..0000000
--- a/pages/_app.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import '../styles/globals.css'
-
-function MyApp({ Component, pageProps }) {
- return
-}
-
-export default MyApp
diff --git a/pages/api/hello.js b/pages/api/hello.js
deleted file mode 100644
index df63de8..0000000
--- a/pages/api/hello.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
-
-export default function handler(req, res) {
- res.status(200).json({ name: 'John Doe' })
-}
diff --git a/pages/index.js b/pages/index.js
deleted file mode 100644
index dc4b640..0000000
--- a/pages/index.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import Head from 'next/head'
-import Image from 'next/image'
-import styles from '../styles/Home.module.css'
-
-export default function Home() {
- return (
-
-
-
Create Next App
-
-
-
-
-
-
-
-
- Get started by editing{' '}
- pages/index.js
-
-
-
-
-
-
-
- )
-}
diff --git a/public/favicon.ico b/public/favicon.ico
index 718d6fe..85671bd 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/public/vercel.svg b/public/vercel.svg
deleted file mode 100644
index fbf0e25..0000000
--- a/public/vercel.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/AddProjectModal/index.js b/src/components/AddProjectModal/index.js
new file mode 100644
index 0000000..d79f144
--- /dev/null
+++ b/src/components/AddProjectModal/index.js
@@ -0,0 +1,336 @@
+import { useState } from 'react'
+import {
+ Button,
+ Text,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalCloseButton,
+ ModalBody,
+ ModalFooter,
+ useDisclosure,
+ FormControl,
+ FormLabel,
+ Input,
+ Textarea,
+} from '@chakra-ui/react'
+import { Select } from 'chakra-react-select'
+import { useSession } from 'next-auth/react'
+import { useRouter } from 'next/router'
+
+function AddProjectModal({ reachedMaximumPostedProjects }) {
+ const { isOpen, onOpen, onClose } = useDisclosure()
+ const { data: session } = useSession()
+ const router = useRouter()
+ const [isLoading, setIsLoading] = useState(false)
+ const [projectTitleExists, setProjectTitleExists] = useState(false)
+
+ const options = [
+ { value: 'JavaScript', label: 'JavaScript', colorScheme: 'yellow' },
+ { value: 'TypeScript', label: 'TypeScript', colorScheme: 'blue' },
+ { value: 'React', label: 'React', colorScheme: 'cyan' },
+ { value: 'Node.js', label: 'Node.js', colorScheme: 'green' },
+ { value: 'Express.js', label: 'Express.js', colorScheme: 'blackAlpha' },
+ { value: 'MongoDB', label: 'MongoDB', colorScheme: 'green' },
+ { value: 'Next.js', label: 'Next.js', colorScheme: 'gray' },
+ ]
+
+ const timeZones = [
+ { value: 'UTC+14:00', label: 'UTC+14:00' },
+ { value: 'UTC+13:00', label: 'UTC+13:00' },
+ { value: 'UTC+12:45', label: 'UTC+12:45' },
+ { value: 'UTC+12:00', label: 'UTC+12:00' },
+ { value: 'UTC+11:00', label: 'UTC+11:00' },
+ { value: 'UTC+10:30', label: 'UTC+10:30' },
+ { value: 'UTC+10:00', label: 'UTC+10:00' },
+ { value: 'UTC+9:30', label: 'UTC+9:30' },
+ { value: 'UTC+9:00', label: 'UTC+9:00' },
+ { value: 'UTC+8:45', label: 'UTC+8:45' },
+ { value: 'UTC+8:00', label: 'UTC+8:00' },
+ { value: 'UTC+7:00', label: 'UTC+7:00' },
+ { value: 'UTC+6:30', label: 'UTC+6:30' },
+ { value: 'UTC+6:00', label: 'UTC+6:00' },
+ { value: 'UTC+5:45', label: 'UTC+5:45' },
+ { value: 'UTC+5:30', label: 'UTC+5:30' },
+ { value: 'UTC+5:00', label: 'UTC+5:00' },
+ { value: 'UTC+4:30', label: 'UTC+4:30' },
+ { value: 'UTC+4:00', label: 'UTC+4:00' },
+ { value: 'UTC+3:00', label: 'UTC+3:00' },
+ { value: 'UTC+2:00', label: 'UTC+2:00' },
+ { value: 'UTC+1:00', label: 'UTC+1:00' },
+ { value: 'UTC+0:00', label: 'UTC+0:00' },
+ { value: 'UTC-1:00', label: 'UTC-1:00' },
+ { value: 'UTC-2:00', label: 'UTC-2:00' },
+ { value: 'UTC-2:30', label: 'UTC-2:30' },
+ { value: 'UTC-3:00', label: 'UTC-3:00' },
+ { value: 'UTC-4:00', label: 'UTC-4:00' },
+ { value: 'UTC-5:00', label: 'UTC-5:00' },
+ { value: 'UTC-6:00', label: 'UTC-6:00' },
+ { value: 'UTC-7:00', label: 'UTC-7:00' },
+ { value: 'UTC-8:00', label: 'UTC-8:00' },
+ { value: 'UTC-9:00', label: 'UTC-9:00' },
+ { value: 'UTC-9:30', label: 'UTC-9:30' },
+ { value: 'UTC-10:00', label: 'UTC-10:00' },
+ { value: 'UTC-11:00', label: 'UTC-11:00' },
+ { value: 'UTC-12:00', label: 'UTC-12:00' },
+ ]
+
+ const inputMarginBottom = '1rem'
+ const labelMarginBottom = '0'
+
+ // Input Values
+ const [title, setTitle] = useState('')
+ const [technologies, setTechnologies] = useState('')
+ const [timezone, setTimezone] = useState({})
+ const [details, setDetails] = useState('')
+
+ // Input didFocusOn
+ const [didFocusOnTitle, setDidFocusOnTitle] = useState(false)
+ const [didFocusOnTechnologies, setDidFocusOnTechnologies] = useState(false)
+ const [didFocusOnTimezone, setDidFocusOnTimezone] = useState(false)
+ const [didFocusOnDetails, setDidFocusOnDetails] = useState(false)
+
+ // Input Validation
+ // a) Required Inputs
+ const titleIsValid =
+ title.trim().length >= 5 &&
+ title.length <= 50 &&
+ title.trim().length !== 0
+
+ const technologiesIsValid = technologies.length > 0
+
+ const timezoneIsValid = Object.keys(timezone).length > 0
+
+ const detailsIsValid =
+ details.length >= 250 &&
+ details.length <= 800 &&
+ details.trim().length !== 0
+
+ //Form Validation
+ const formIsValid =
+ titleIsValid && technologiesIsValid && timezoneIsValid && detailsIsValid
+
+ const resetStates = () => {
+ setTitle('')
+ setTechnologies('')
+ setTimezone({})
+ setDetails('')
+ setDidFocusOnTitle(false)
+ setDidFocusOnTechnologies(false)
+ setDidFocusOnTimezone(false)
+ setDidFocusOnDetails(false)
+ setProjectTitleExists(false)
+ setIsLoading(false)
+ }
+
+ const closeHandler = () => {
+ resetStates()
+ onClose()
+ }
+
+ const formSubmit = async () => {
+ setProjectTitleExists(false)
+ setIsLoading(true)
+ const user_id = session.dbUser._id
+
+ let formData = {
+ timezone,
+ title,
+ technologies,
+ details,
+ admin: user_id,
+ }
+
+ try {
+ const response = await fetch('/api/projects', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(formData),
+ })
+
+ const data = await response.json()
+ if (response.status == '200') {
+ return router.reload()
+ }
+
+ if (response.status == '400') {
+ if (data.error === 'Project Title is already taken') {
+ setProjectTitleExists(true)
+ }
+ }
+ setIsLoading(false)
+ } catch (error) {
+ setIsLoading(false)
+ console.log(
+ 'Something went wrong while trying to add project idea.'
+ )
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+ Project Idea
+
+
+
+
+ Title
+
+ {
+ setDidFocusOnTitle(true)
+ }}
+ onChange={(e) => {
+ setTitle(e.target.value)
+ }}
+ type="text"
+ marginBottom={inputMarginBottom}
+ />
+ {projectTitleExists && (
+
+ Project already exists.
+
+ )}
+ {didFocusOnTitle && (
+
+ Between 5 and 50 characters.
+
+ )}
+
+
+ Technologies
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default AddProjectModal
diff --git a/src/components/BannedCard/index.js b/src/components/BannedCard/index.js
new file mode 100644
index 0000000..1090bdd
--- /dev/null
+++ b/src/components/BannedCard/index.js
@@ -0,0 +1,26 @@
+import { Flex, Spacer, Heading, Text } from '@chakra-ui/react'
+
+function BannedCard({ reason }) {
+ return (
+
+
+ Your account has been suspended for violating the community
+ standards.
+
+
+ {`"${reason}"`}
+
+ )
+}
+
+export default BannedCard
diff --git a/src/components/CommunityStandards/index.js b/src/components/CommunityStandards/index.js
new file mode 100644
index 0000000..46fa8ea
--- /dev/null
+++ b/src/components/CommunityStandards/index.js
@@ -0,0 +1,120 @@
+import { useState } from 'react'
+import { Flex, Heading, Text, Button, Checkbox } from '@chakra-ui/react'
+
+function CommunityStandards({ onProceed }) {
+ const [agreeToTerms, setAgreeToTerms] = useState(false)
+
+ const rules = [
+ {
+ id: 1,
+ title: 'Creating a Friendly and Open Community',
+ content: [
+ {
+ paragraph: 1,
+ body: 'Any conduct that threatens a member, including (but not limited to) harassment, bullying, discrimination of any type, hate speech, and posting of inappropriate material, is not tolerated - period!',
+ },
+ {
+ paragraph: 2,
+ body: 'Members are expected to be tolerant of other cultures and opinions, and to conduct themselves in a professional manner. Disagreeing is acceptable, but being disrespectful is not. You should treat others how you want to be treated.',
+ },
+ ],
+ },
+ {
+ id: 2,
+ title: 'Cultivating a Supportive Community',
+ content: [
+ {
+ paragraph: 1,
+ body: 'Being a Chingu means you have a responsibility to support your Voyage team by being an active participant in team meetings, completing commitments, and conducting yourself in a professional manner.',
+ },
+ {
+ paragraph: 2,
+ body: 'Chingus respect their peers by not plagiarizing. This includes any original work created by someone else including articles, code, and ideas.',
+ },
+ {
+ paragraph: 3,
+ body: 'Chingus also have the responsibility to actively participate in our forums to help fellow Chingus on their learning path by answering questions, helping to motivate others, sharing what they’ve learned, and providing honest and respectful critiques.',
+ },
+ ],
+ },
+ {
+ id: 3,
+ title: 'This is a spam-free community',
+ content: [
+ {
+ paragraph: 1,
+ body: "The focus of this community is to help one another become better Software Developers. To maintain this focus we don't allow spammy posts, including self-promotion. This includes DM'ing others in the community or asking for personal information for the purposes of marketing.",
+ },
+ ],
+ },
+ ]
+
+ // Ensures uniform spacing
+ const ruleMarginBottom = '0.5rem'
+ const paragraphMarginBottom = '1rem'
+
+ const agreeToTermsHandler = (event) => {
+ event.preventDefault()
+ return setAgreeToTerms((prevState) => !prevState)
+ }
+
+ return (
+
+ Community Standards
+
+ We view this as an obligation to all of our members. Any members
+ violating the Community Standards will be removed from the
+ community. Violations and complaints should be reported to
+ admin@chingu.io and will be kept confidential.
+
+ {rules.map((rule) => (
+
+
+ {rule.title}
+
+ {rule.content.map((content) => (
+
+ {content.body}
+
+ ))}
+
+ ))}
+
+
+ I have read over all the community standards and agree to
+ follow them and conduct myself in a professional manner.
+ Additionally, I understand that violating any of these
+ standards will result in a ban from the platform.
+
+
+
+
+ )
+}
+
+export default CommunityStandards
diff --git a/src/components/CreateProfile/index.js b/src/components/CreateProfile/index.js
new file mode 100644
index 0000000..047bef1
--- /dev/null
+++ b/src/components/CreateProfile/index.js
@@ -0,0 +1,367 @@
+import {
+ Flex,
+ Text,
+ Heading,
+ FormControl,
+ FormLabel,
+ Input,
+ RadioGroup,
+ Radio,
+ HStack,
+ Button,
+ Tooltip,
+ List,
+ ListItem,
+ ListIcon,
+} from '@chakra-ui/react'
+import { AiOutlineInfoCircle } from 'react-icons/ai'
+import { useState } from 'react'
+import { useSession } from 'next-auth/react'
+import { useRouter } from 'next/router'
+import { MdCheckCircle, MdRemoveCircle } from 'react-icons/md'
+
+function CreateProfile() {
+ const [usernameExists, setUsernameExists] = useState(false)
+ const [emailExists, setEmailExists] = useState(false)
+ const [isLoading, setIsLoading] = useState(false)
+ const router = useRouter()
+ const { data: session } = useSession()
+
+ const inputMarginBottom = '1rem'
+ const labelMarginBottom = '0'
+
+ // Input Values
+ const [username, setUsername] = useState('')
+ const [location, setLocation] = useState('')
+ const [email, setEmail] = useState('')
+ const [discordId, setDiscordId] = useState('')
+ const [methodOfContact, setMethodOfContact] = useState('email')
+ const [githubLink, setGithubLink] = useState('')
+
+ // Input didFocusOn
+ const [didFocusOnUsername, setDidFocusOnUsername] = useState(false)
+ const [didFocusOnEmail, setDidFocusOnEmail] = useState(false)
+ const [didFocusOnDiscordId, setDidFocusOnDiscordId] = useState(false)
+
+ // Input Validation
+ // a) Required Inputs
+ const usernameIsValid =
+ username.length > 4 && username.length < 17 && !username.includes(' ')
+
+ const emailIsValid = email.includes('@') && email.includes('.')
+
+ const discordIdIsValid = discordId.includes('#')
+
+ const methodOfContactIsValid =
+ methodOfContact === 'email' || methodOfContact === 'discord'
+ ? true
+ : false
+
+ // b) Optional Inputs
+ const locationIsValid =
+ location.length > 0 ? location.includes(', ') : location === ''
+
+ const githubLinkIsValid =
+ githubLink.length > 0
+ ? githubLink.includes('https://github.com/')
+ : githubLink === ''
+
+ //Form Validation
+ const formIsValid =
+ usernameIsValid &&
+ locationIsValid &&
+ emailIsValid &&
+ discordIdIsValid &&
+ methodOfContactIsValid &&
+ githubLinkIsValid
+
+ const formSubmit = async () => {
+ let formData = {
+ authenticatedDiscordId: session.userId,
+ discordAvatarUrl: session.user.image,
+ username,
+ location,
+ email,
+ discordUsername: discordId,
+ preferredMethodOfContact: methodOfContact,
+ githubLink,
+ isBanned: { value: false, reason: '' },
+ }
+
+ if (formIsValid) {
+ setUsernameExists(false)
+ setEmailExists(false)
+ setIsLoading(true)
+ try {
+ const response = await fetch('/api/user', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(formData),
+ })
+
+ const data = await response.json()
+ if (response.status == '200') {
+ return router.reload()
+ }
+
+ if (response.status == '400') {
+ if (data.error === 'Username is already taken') {
+ setUsernameExists(true)
+ }
+
+ if (
+ data.error ===
+ 'This email is already link to another account'
+ ) {
+ setEmailExists(true)
+ }
+
+ if (data.error === 'Both username and email is taken') {
+ setUsernameExists(true)
+ setEmailExists(true)
+ }
+ }
+
+ setIsLoading(false)
+ } catch (error) {
+ setIsLoading(false)
+ console.log(error)
+ console.log('1. Something went wrong.')
+ }
+ }
+ }
+
+ return (
+
+ Member
+
+ Please be aware that your profile information will be visible to
+ other members when requesting to join their projects on this
+ platform.
+
+
+ Username
+ {
+ setDidFocusOnUsername(true)
+ }}
+ onChange={(e) => {
+ setUsername(e.target.value.toLowerCase())
+ }}
+ type="text"
+ marginBottom={inputMarginBottom}
+ />
+ {usernameExists && (
+
+ Username is already taken.
+
+ )}
+ {didFocusOnUsername && (
+
+
+ 4 && username.length < 17
+ ? MdCheckCircle
+ : MdRemoveCircle
+ }
+ color={
+ username.length > 4 && username.length < 17
+ ? 'green.500'
+ : 'red.500'
+ }
+ />
+
+ Between 5 and 16 characters
+
+
+
+
+ No Spaces
+
+
+ )}
+
+
+
+ Location
+
+ (optional)
+
+ {
+ setLocation(e.target.value)
+ }}
+ type="text"
+ marginBottom={inputMarginBottom}
+ />
+
+
+
+ Email
+
+
+
+
+
+
+
+ {
+ setDidFocusOnEmail(true)
+ }}
+ onChange={(e) => {
+ setEmail(e.target.value.toLowerCase())
+ }}
+ type="email"
+ marginBottom={inputMarginBottom}
+ />
+ {emailExists && (
+
+ Email is already linked to another account.
+
+ )}
+
+
+
+ Discord ID
+
+
+
+
+
+
+
+ {
+ setDidFocusOnDiscordId(true)
+ }}
+ onChange={(e) => {
+ setDiscordId(e.target.value)
+ }}
+ type="text"
+ marginBottom={inputMarginBottom}
+ />
+
+
+ Preferred Method of Contact
+
+
+ You are not guaranteed to be contacted by your preferred
+ method of contact.
+
+
+
+ {
+ setMethodOfContact(e.target.value)
+ }}
+ value="email"
+ >
+ Email
+
+ {
+ setMethodOfContact(e.target.value)
+ }}
+ value="discord"
+ >
+ Discord
+
+
+
+
+
+
+ Github
+
+ (recommended)
+
+ {
+ setGithubLink(e.target.value)
+ }}
+ type="text"
+ marginBottom={inputMarginBottom}
+ isInvalid={!githubLinkIsValid}
+ />
+
+
+
+ )
+}
+
+export default CreateProfile
diff --git a/src/components/DetailsPreviewCard/index.js b/src/components/DetailsPreviewCard/index.js
new file mode 100644
index 0000000..63b0921
--- /dev/null
+++ b/src/components/DetailsPreviewCard/index.js
@@ -0,0 +1,442 @@
+import { useSession } from 'next-auth/react'
+import useSWR from 'swr'
+import { useState } from 'react'
+import {
+ Flex,
+ Box,
+ Heading,
+ Text,
+ VStack,
+ Avatar,
+ AvatarGroup,
+ Button,
+ Accordion,
+ AccordionItem,
+ AccordionButton,
+ AccordionPanel,
+ AccordionIcon,
+} from '@chakra-ui/react'
+import { useRouter } from 'next/router'
+import { BiHourglass, BiTimeFive } from 'react-icons/bi'
+import MemberCard from '../MemberCard'
+import { deleteProjectIdea, patchProject } from '../../controllers/project'
+import {
+ getNumberOfProjectsRequested,
+ getRelativeProjectDates,
+ formatRelativeProjectDates,
+} from '../util.js'
+
+function DetailsPreviewCard({ info, projects }) {
+ const [projectRequestLoading, setProjectRequestLoading] = useState(false)
+
+ const { data: session } = useSession()
+ const router = useRouter()
+
+ const isAdmin = info?.admin?._id === session?.dbUser?._id
+
+ const requestedMembers = info?.requestedMembers?.map((member) => member._id)
+ const isRequestedMember = requestedMembers?.includes(session?.dbUser._id)
+
+ const currentMembers = info?.currentMembers?.map((member) => member._id)
+ const isCurrentMember = currentMembers?.includes(session?.dbUser._id)
+
+ const JOINLIMIT = process.env.NEXT_PUBLIC_JOINLIMIT
+
+ const projectsRequested = getNumberOfProjectsRequested(projects, session)
+
+ const isRequestable = projectsRequested < JOINLIMIT
+
+ const isJoinable = !isRequestedMember && isRequestable && !isCurrentMember
+
+ const isReported = false
+
+ const { expiresMessage, createdMessage } = formatRelativeProjectDates(
+ getRelativeProjectDates(info)
+ )
+
+ const requestForProject = async () => {
+ try {
+ setProjectRequestLoading(true)
+ const formDataProject = {
+ user_id: session?.dbUser?._id,
+ requestType: 'requestForProject',
+ }
+
+ const response = await patchProject(info._id, formDataProject)
+ if (response == true) {
+ router.reload()
+ } else {
+ setProjectRequestLoading(false)
+ console.log(
+ 'Something went wrong while trying to request to join a project.'
+ )
+ }
+ } catch (e) {
+ console.log(e)
+ }
+ }
+
+ const withdrawFromProject = async () => {
+ try {
+ setProjectRequestLoading(true)
+ const formDataProject = {
+ user_id: session?.dbUser?._id,
+ requestType: 'withdrawFromProject',
+ }
+
+ const response = await patchProject(info._id, formDataProject)
+ if (response == true) {
+ router.reload()
+ } else {
+ setProjectRequestLoading(false)
+ console.log(
+ 'Something went wrong while trying to withdraw from a project.'
+ )
+ }
+ } catch (e) {
+ console.log(e)
+ }
+ }
+
+ const requestHandler = async () => {
+ if (isJoinable) {
+ return await requestForProject()
+ }
+ if (isRequestedMember) {
+ return await withdrawFromProject()
+ }
+ // If limit reached
+ return
+ }
+
+ const approveHandler = async (id, projectId) => {
+ const formDataProject = {
+ user_id: id,
+ requestType: 'approveProject',
+ }
+
+ const response = await patchProject(projectId, formDataProject)
+ if (response == true) {
+ router.reload()
+ } else {
+ console.log(
+ 'Something went wrong while trying to approve a member.'
+ )
+ }
+ }
+
+ const rejectHandler = async (id, projectId) => {
+ const formDataProject = {
+ user_id: id,
+ requestType: 'rejectProject',
+ }
+
+ const response = await patchProject(projectId, formDataProject)
+ if (response == true) {
+ router.reload()
+ } else {
+ console.log('Something went wrong while trying to reject a member.')
+ }
+ }
+
+ const fetcher = (...args) => fetch(...args).then((res) => res.json())
+
+ const { data, error } = useSWR('/api/projects', fetcher)
+
+ if (info !== undefined) {
+ if (isAdmin) {
+ const numberOfRequestedMembers = info?.requestedMembers?.length
+ const numberOfCurrentMembers = info?.currentMembers?.length
+
+ return (
+
+ {info?.title}
+
+
+
+
+ {info?.admin?.username}
+
+
+
+
+
+ {info?.timezone}
+
+
+
+
+
+
+ {expiresMessage}
+
+
+
+ You will have to contact members before the post expires
+ or you risk losing their contact information along with
+ this post.
+
+
+
+ {`Requested Members (${numberOfRequestedMembers})`}
+
+ {info?.requestedMembers?.map((member, index) => {
+ return (
+
+ approveHandler(id, info._id)
+ }
+ onReject={(id) =>
+ rejectHandler(id, info._id)
+ }
+ />
+ )
+ })}
+
+
+
+ {`Current Members (${numberOfCurrentMembers})`}
+
+ {info?.currentMembers?.map((member, index) => {
+ return
+ })}
+
+
+
+
+
+
+
+
+ Description
+
+
+
+
+
+
+ {info.details}
+
+
+
+
+ {createdMessage}
+
+
+ )
+ }
+
+ if (!isAdmin) {
+ return (
+
+ {info?.title}
+
+
+
+
+
+ {info?.admin?.username}
+
+
+
+
+
+ {info?.timezone}
+
+
+
+
+
+
+ {expiresMessage}
+
+
+
+
+
+
+ {info?.technologies?.length > 1
+ ? 'Technologies'
+ : 'Technology'}
+
+ {info?.technologies?.map((tech, index) => (
+
+ {tech}
+
+ ))}
+
+
+
+ Description
+
+ {info?.details}
+
+
+
+
+ Project Insights
+
+
+ Requested Members
+
+ {info?.requestedMembers?.length > 0 && (
+
+ {info.requestedMembers.map((member, index) => {
+ return (
+
+ )
+ })}
+
+ )}
+
+ Current Members
+
+ {info?.currentMembers?.length > 0 && (
+
+ {info.currentMembers.map((member, index) => {
+ return (
+
+ )
+ })}
+
+ )}
+
+
+ {createdMessage}
+
+
+ )
+ }
+ }
+}
+
+export default DetailsPreviewCard
diff --git a/src/components/DiscordButton/DiscordButton.module.css b/src/components/DiscordButton/DiscordButton.module.css
new file mode 100644
index 0000000..d1a7768
--- /dev/null
+++ b/src/components/DiscordButton/DiscordButton.module.css
@@ -0,0 +1,25 @@
+.discord-button .icon {
+ width: 25px;
+ height: 25px;
+ margin-right: 15px;
+}
+
+.discord-button .icon svg {
+ fill: white;
+}
+
+.discord-button a {
+ color: white;
+ font-weight: bold;
+ border-radius: 8px;
+ display: inline-flex;
+ align-items: center;
+ padding: 10px 15px;
+ background-color: #7289da;
+ text-decoration: none;
+}
+
+.discord-button a:hover {
+ background-color: #6a7fc9;
+ cursor: pointer;
+}
diff --git a/src/components/DiscordButton/index.js b/src/components/DiscordButton/index.js
new file mode 100644
index 0000000..454b4b4
--- /dev/null
+++ b/src/components/DiscordButton/index.js
@@ -0,0 +1,29 @@
+import styles from './DiscordButton.module.css'
+
+function DiscordButton({ onClick }) {
+ return (
+
+ )
+}
+
+export default DiscordButton
diff --git a/src/components/Footer/Chingu-Logo.png b/src/components/Footer/Chingu-Logo.png
new file mode 100644
index 0000000..31612a5
Binary files /dev/null and b/src/components/Footer/Chingu-Logo.png differ
diff --git a/src/components/Footer/index.js b/src/components/Footer/index.js
new file mode 100644
index 0000000..989bb1a
--- /dev/null
+++ b/src/components/Footer/index.js
@@ -0,0 +1,77 @@
+import { Flex, Box } from '@chakra-ui/react'
+import {
+ ButtonGroup,
+ Container,
+ IconButton,
+ Stack,
+ Text,
+ Image,
+} from '@chakra-ui/react'
+import { FaGlobe, FaGithub, FaLinkedin, FaTwitter } from 'react-icons/fa'
+import Logo from './Chingu-Logo.png'
+export default function Footer() {
+ return (
+
+
+
+
+
+
+
+ }
+ />
+ }
+ />
+ }
+ />
+
+ }
+ />
+
+
+
+ Made with ❤ from a Chingu cohort.
+
+
+
+ )
+}
diff --git a/src/components/HamburgerMenu/index.js b/src/components/HamburgerMenu/index.js
new file mode 100644
index 0000000..600ae0e
--- /dev/null
+++ b/src/components/HamburgerMenu/index.js
@@ -0,0 +1,69 @@
+import { useState } from 'react'
+import {
+ Heading,
+ Drawer,
+ DrawerBody,
+ DrawerOverlay,
+ DrawerContent,
+ DrawerCloseButton,
+ IconButton,
+ Button,
+ useDisclosure,
+ Link,
+} from '@chakra-ui/react'
+import NextLink from 'next/link'
+import { GiHamburgerMenu } from 'react-icons/gi'
+import { signOut } from 'next-auth/react'
+
+function HamburgerMenu({ routes }) {
+ const [size, setSize] = useState('xs')
+ const { isOpen, onOpen, onClose } = useDisclosure()
+
+ const handleClick = (newSize) => {
+ setSize(newSize)
+ onOpen()
+ }
+
+ return (
+
+ handleClick(size)}
+ colorScheme="gray"
+ variant="ghost"
+ icon={}
+ />
+
+
+
+
+
+
+ {routes.map((route, index) => {
+ return (
+
+
+
+ {route.name}
+
+
+
+ )
+ })}
+
+
+
+
+ )
+}
+
+export default HamburgerMenu
diff --git a/src/components/Header/index.js b/src/components/Header/index.js
new file mode 100644
index 0000000..f49faac
--- /dev/null
+++ b/src/components/Header/index.js
@@ -0,0 +1,11 @@
+import Head from 'next/head'
+
+export default function Header() {
+ return (
+
+ Chingu Collaborate
+
+
+
+ )
+}
diff --git a/src/components/LimitsOverview/index.js b/src/components/LimitsOverview/index.js
new file mode 100644
index 0000000..b872144
--- /dev/null
+++ b/src/components/LimitsOverview/index.js
@@ -0,0 +1,76 @@
+import { Flex, Heading, Text, Tooltip } from '@chakra-ui/react'
+import { AiOutlineInfoCircle } from 'react-icons/ai'
+
+function LimitsOverview({ projectsCreated, projectsRequested }) {
+ const CREATELIMIT = process.env.NEXT_PUBLIC_POSTLIMIT
+ const JOINLIMIT = process.env.NEXT_PUBLIC_JOINLIMIT
+
+ return (
+
+
+ Project Limits
+
+
+
+
+
+
+
+
+ {`${projectsCreated}/${CREATELIMIT}`}
+
+ Created
+
+
+
+
+ {`${projectsRequested}/${JOINLIMIT}`}
+
+ Requested
+
+
+
+
+ )
+}
+
+export default LimitsOverview
diff --git a/src/components/ManageProject/index.js b/src/components/ManageProject/index.js
new file mode 100644
index 0000000..087a244
--- /dev/null
+++ b/src/components/ManageProject/index.js
@@ -0,0 +1,198 @@
+import {
+ Button,
+ Flex,
+ Box,
+ Heading,
+ Text,
+ VStack,
+ Accordion,
+ AccordionItem,
+ AccordionButton,
+ AccordionPanel,
+ AccordionIcon,
+ Avatar,
+} from '@chakra-ui/react'
+
+import { BiTimeFive, BiHourglass } from 'react-icons/bi'
+import { useRouter } from 'next/router'
+import MemberCard from '../MemberCard'
+import { deleteProjectIdea } from '../../controllers/project'
+import { patchProject } from '../../controllers/project'
+import { getRelativeProjectDates, formatRelativeProjectDates } from '../util.js'
+
+function ManageProject({ project }) {
+ const { expiresMessage, createdMessage } = formatRelativeProjectDates(
+ getRelativeProjectDates(project)
+ )
+
+ const numberOfRequestedMembers = project?.requestedMembers?.length
+ const numberOfCurrentMembers = project?.currentMembers?.length
+
+ const router = useRouter()
+
+ const approveHandler = async (id, projectId) => {
+ try {
+ const formDataProject = {
+ user_id: id,
+ requestType: 'approveProject',
+ }
+
+ const response = await patchProject(projectId, formDataProject)
+ if (response == true) {
+ router.reload()
+ } else {
+ console.log(
+ 'Something went wrong while trying to approve a member.'
+ )
+ }
+ } catch (e) {
+ console.log(e)
+ }
+ }
+ const rejectHandler = async (id, projectId) => {
+ try {
+ const formDataProject = {
+ user_id: id,
+ requestType: 'rejectProject',
+ }
+
+ const response = await patchProject(projectId, formDataProject)
+ if (response == true) {
+ router.reload()
+ } else {
+ console.log(
+ 'Something went wrong while trying to reject a member.'
+ )
+ }
+ } catch (e) {
+ console.log(e)
+ }
+ }
+
+ return (
+
+
+ {project?.title}
+
+
+
+
+
+ {project?.admin?.username}
+
+
+
+
+
+ {project.timezone}
+
+
+
+
+
+
+ {expiresMessage}
+
+
+
+ You will have to contact members before the post expires or you
+ risk losing their contact information along with this post.
+
+
+
+ {`Requested Members (${numberOfRequestedMembers})`}
+
+ {project?.requestedMembers?.map((member, index) => {
+ return (
+
+ approveHandler(id, project._id)
+ }
+ onReject={(id) =>
+ rejectHandler(id, project._id)
+ }
+ />
+ )
+ })}
+
+
+
+ {`Current Members (${numberOfCurrentMembers})`}
+
+ {project?.currentMembers?.map((member, index) => {
+ return
+ })}
+
+
+
+
+
+
+
+
+ Description
+
+
+
+
+
+
+ {project.details}
+
+
+
+
+ {createdMessage}
+
+
+ )
+}
+
+export default ManageProject
diff --git a/src/components/MemberCard/index.js b/src/components/MemberCard/index.js
new file mode 100644
index 0000000..0f94c74
--- /dev/null
+++ b/src/components/MemberCard/index.js
@@ -0,0 +1,125 @@
+import { useState } from 'react'
+import {
+ Avatar,
+ Button,
+ ButtonGroup,
+ Flex,
+ Heading,
+ Text,
+} from '@chakra-ui/react'
+import { BiCheck, BiX } from 'react-icons/bi'
+
+function RequestedMemberCard({ info, isRequestable, onApprove, onReject }) {
+ const [disableActions, setDisableActions] = useState(false)
+
+ const approveMember = (info) => {
+ setDisableActions(true)
+ onApprove(info?._id)
+ }
+
+ const rejectMember = (info) => {
+ setDisableActions(true)
+ onReject(info?._id)
+ }
+
+ const locationIsValid =
+ info.location !== undefined &&
+ info.location.trim().length > 0 &&
+ info.location !== ''
+
+ const githubLinkIsValid =
+ info.githubLink !== undefined && info.githubLink !== ''
+
+ return (
+
+
+
+
+
+ {info.username}
+
+ {isRequestable && (
+
+
+
+
+ )}
+
+
+
+
+ Discord
+
+ {info.discordUsername}
+
+
+
+
+ Email
+
+ {info.email}
+
+
+ {githubLinkIsValid && (
+
+
+ Github
+
+ {info.githubLink}
+
+ )}
+
+
+
+ )
+}
+
+export default RequestedMemberCard
diff --git a/src/components/Navbar/ChinguCollaborateLogo.png b/src/components/Navbar/ChinguCollaborateLogo.png
new file mode 100644
index 0000000..7591b7a
Binary files /dev/null and b/src/components/Navbar/ChinguCollaborateLogo.png differ
diff --git a/src/components/Navbar/index.js b/src/components/Navbar/index.js
new file mode 100644
index 0000000..ad6d6b4
--- /dev/null
+++ b/src/components/Navbar/index.js
@@ -0,0 +1,85 @@
+import NextLink from 'next/link'
+import Image from 'next/image'
+import logo from './ChinguCollaborateLogo.png'
+import HamburgerMenu from '../HamburgerMenu'
+import { Flex, Box, Link, Divider } from '@chakra-ui/react'
+import { signOut, useSession } from 'next-auth/react'
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/router'
+function Navbar() {
+ const nonAuthenticatedRoutes = []
+
+ const authenticatedRoutes = [
+ { name: 'Projects', route: '/projects' },
+ { name: 'Sign Out', route: '/' },
+ ]
+
+ const router = useRouter()
+ const { data: session, status } = useSession()
+
+ const [routes, setRoutes] = useState(nonAuthenticatedRoutes)
+
+ useEffect(() => {
+ if (session) {
+ setRoutes(authenticatedRoutes)
+ }
+ }, [session])
+
+ const redirectHandler = () => {
+ if (router.pathname === '/projects') {
+ return router.reload()
+ }
+ router.replace('/projects')
+ }
+
+ return (
+ <>
+
+
+
+
+ {/* Smaller Screens */}
+
+ {routes.length > 0 && }{' '}
+
+ {/* Larger Screens */}
+
+ {routes.map((route, index) => {
+ return (
+
+
+ {route.name}
+
+
+ )
+ })}
+
+
+
+ >
+ )
+}
+
+export default Navbar
diff --git a/src/components/Pagination/index.js b/src/components/Pagination/index.js
new file mode 100644
index 0000000..d21991d
--- /dev/null
+++ b/src/components/Pagination/index.js
@@ -0,0 +1,40 @@
+import { ButtonGroup, Button } from '@chakra-ui/react'
+
+export default function Pagination({ page, onChange, totalProjectPages }) {
+ const clickHandler = (event) => onChange(event.target.value)
+
+ const { pageRange: range, selectedPage } = page
+
+ return (
+
+ {/* Should be disabled if selected page is at 1 */}
+ {selectedPage > 1 && (
+
+ )}
+ {range?.map((number) => (
+
+ ))}
+ {/* Should be disabled if selected page is at max*/}
+ {selectedPage < totalProjectPages && (
+
+ )}
+
+ )
+}
diff --git a/src/components/ProjectActions/index.js b/src/components/ProjectActions/index.js
new file mode 100644
index 0000000..6ed9eca
--- /dev/null
+++ b/src/components/ProjectActions/index.js
@@ -0,0 +1,62 @@
+import {
+ Flex,
+ InputGroup,
+ Input,
+ InputLeftElement,
+ Button,
+ InputRightElement,
+} from '@chakra-ui/react'
+import { BiSearch } from 'react-icons/bi'
+import AddProjectModal from '../AddProjectModal'
+import { useRef } from 'react'
+
+function ProjectActions({ reachedMaximumPostedProjects, onSearch }) {
+ const searchInput = useRef('')
+
+ return (
+
+
+
+
+
+ {
+ if (event.code === 'Enter') {
+ onSearch(searchInput.current.value)
+ }
+ }}
+ type="search"
+ borderWidth="2px"
+ borderRadius="md"
+ fontSize="md"
+ size="lg"
+ placeholder="Enter a project title"
+ />
+
+
+
+
+
+
+ )
+}
+
+export default ProjectActions
diff --git a/src/components/ProjectDetails/index.js b/src/components/ProjectDetails/index.js
new file mode 100644
index 0000000..ce3e015
--- /dev/null
+++ b/src/components/ProjectDetails/index.js
@@ -0,0 +1,250 @@
+import {
+ Flex,
+ Heading,
+ Text,
+ Avatar,
+ AvatarGroup,
+ Button,
+} from '@chakra-ui/react'
+import { BiTimeFive, BiHourglass } from 'react-icons/bi'
+import { useSession } from 'next-auth/react'
+import { useState } from 'react'
+import { useRouter } from 'next/router'
+import { getRelativeProjectDates, formatRelativeProjectDates } from '../util.js'
+import { patchProject } from '../../controllers/project'
+
+function ProjectDetails({ project, isJoinable }) {
+ const [isLoading, setIsLoading] = useState(false)
+
+ const router = useRouter()
+ const { data: session } = useSession()
+
+ const { expiresMessage, createdMessage } = formatRelativeProjectDates(
+ getRelativeProjectDates(project)
+ )
+
+ const requestedMembers = project?.requestedMembers?.map(
+ (member) => member._id
+ )
+ const isRequestedMember = requestedMembers?.includes(session?.dbUser._id)
+
+ const currentMembers = project?.currentMembers?.map((member) => member._id)
+ const isCurrentMember = currentMembers?.includes(session?.dbUser._id)
+
+ const isReported = false
+
+ const requestForProject = async () => {
+ try {
+ setIsLoading(true)
+ const formDataProject = {
+ user_id: session?.dbUser?._id,
+ requestType: 'requestForProject',
+ }
+
+ const response = await patchProject(project._id, formDataProject)
+ if (response == true) {
+ router.reload()
+ } else {
+ setIsLoading(false)
+ console.log(
+ 'Something went wrong while trying to request to join a project.'
+ )
+ }
+ } catch (e) {
+ console.log(e)
+ }
+ }
+
+ const withdrawFromProject = async () => {
+ try {
+ setIsLoading(true)
+ const formDataProject = {
+ user_id: session?.dbUser?._id,
+ requestType: 'withdrawFromProject',
+ }
+
+ const response = await patchProject(project._id, formDataProject)
+ if (response == true) {
+ router.reload()
+ } else {
+ setIsLoading(false)
+ console.log(
+ 'Something went wrong while trying to withdraw from a project.'
+ )
+ }
+ } catch (e) {
+ console.log(e)
+ }
+ }
+
+ const requestHandler = async () => {
+ if (isJoinable) {
+ return await requestForProject()
+ }
+ if (isRequestedMember) {
+ return await withdrawFromProject()
+ }
+ // If limit reached
+ return
+ }
+
+ return (
+
+
+ {project?.title}
+
+
+
+
+ {`${project?.admin?.username}`}
+
+
+
+
+ {project?.timezone}
+
+
+
+
+
+
+ {expiresMessage}
+
+
+
+
+
+
+
+ {project.technologies.length > 1
+ ? 'Technologies'
+ : 'Technology'}
+
+
+ {project?.technologies.map((tech, index) => (
+
+ {tech}
+
+ ))}
+
+
+
+
+ Description
+
+ {project?.details}
+
+
+
+
+ Project Insights
+
+
+ Requested Members
+
+ {project.requestedMembers.length > 0 && (
+
+ {project.requestedMembers.map((member, index) => {
+ return (
+
+ )
+ })}
+
+ )}
+
+ Current Members
+
+ {project.currentMembers.length > 0 && (
+
+ {project.currentMembers.map((member, index) => {
+ return (
+
+ )
+ })}
+
+ )}
+
+
+ {createdMessage}
+
+
+ )
+}
+
+export default ProjectDetails
diff --git a/src/components/ProjectPreviewCard/index.js b/src/components/ProjectPreviewCard/index.js
new file mode 100644
index 0000000..67ba393
--- /dev/null
+++ b/src/components/ProjectPreviewCard/index.js
@@ -0,0 +1,150 @@
+import {
+ Flex,
+ Heading,
+ Text,
+ IconButton,
+ Menu,
+ MenuButton,
+ MenuList,
+ MenuItem,
+ HStack,
+ Tag,
+ TagLabel,
+ LinkBox,
+ LinkOverlay,
+ Avatar,
+} from '@chakra-ui/react'
+import { BsThreeDotsVertical } from 'react-icons/bs'
+import { BiTimeFive } from 'react-icons/bi'
+import { useRouter } from 'next/router'
+import { useSession } from 'next-auth/react'
+import { deleteProjectIdea } from '../../controllers/project'
+
+function ProjectPreviewCard({ project, isSelected, externalDetails, onClick }) {
+ const router = useRouter()
+
+ const { data: session } = useSession()
+
+ const isAdmin = project?.admin?._id === session.dbUser._id
+
+ const selectedProjectHandler = () => {
+ if (externalDetails) {
+ return
+ }
+ onClick()
+ }
+
+ return (
+
+
+
+ {/* If the viewport is above medium, onClick changes selectedProject */}
+
+
+ {project?.title}
+
+
+
+
+
+
+
+
+ {project?.admin?.username}
+
+
+ {project?.timezone && (
+
+
+
+ {project?.timezone}
+
+
+ )}
+
+
+ {project?.technologies?.map(
+ (tech, index) =>
+ index < 3 && (
+
+ {tech}
+
+ )
+ )}
+ {project?.technologies?.length > 3 ? (
+
+ +
+
+ ) : (
+ ''
+ )}
+
+
+ {project?.details}
+
+
+
+ )
+}
+
+export default ProjectPreviewCard
diff --git a/src/components/Wrapper/ChinguCollaborateLogo.png b/src/components/Wrapper/ChinguCollaborateLogo.png
new file mode 100644
index 0000000..7591b7a
Binary files /dev/null and b/src/components/Wrapper/ChinguCollaborateLogo.png differ
diff --git a/src/components/Wrapper/ExistingUser.js b/src/components/Wrapper/ExistingUser.js
new file mode 100644
index 0000000..67d08e7
--- /dev/null
+++ b/src/components/Wrapper/ExistingUser.js
@@ -0,0 +1,24 @@
+import { useState } from 'react'
+import Header from '../Header'
+import Navbar from '../Navbar'
+import BannedCard from '../BannedCard'
+import Footer from '../Footer'
+
+export default function ExistingUser({ children, isBanned }) {
+ return (
+ <>
+
+
+
+
+ {isBanned.value ? (
+
+ ) : (
+ children
+ )}
+
+
+
+ >
+ )
+}
diff --git a/src/components/Wrapper/Loading.js b/src/components/Wrapper/Loading.js
new file mode 100644
index 0000000..de4f37f
--- /dev/null
+++ b/src/components/Wrapper/Loading.js
@@ -0,0 +1,35 @@
+import Header from '../Header'
+import { Flex, Spinner } from '@chakra-ui/react'
+import Image from 'next/image'
+import logo from './ChinguCollaborateLogo.png'
+
+export default function Loading() {
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/components/Wrapper/NewUser.js b/src/components/Wrapper/NewUser.js
new file mode 100644
index 0000000..bfbb26c
--- /dev/null
+++ b/src/components/Wrapper/NewUser.js
@@ -0,0 +1,28 @@
+import { useState } from 'react'
+import Header from '../Header'
+import Navbar from '../Navbar'
+import CommunityStandards from '../CommunityStandards'
+import CreateProfile from '../CreateProfile'
+import Footer from '../Footer'
+
+export default function NewUser() {
+ const [agreeToRules, setAgreeToRules] = useState(false)
+ const proceedHandler = () => {
+ setAgreeToRules(true)
+ }
+ return (
+ <>
+
+
+
+
+ {!agreeToRules && (
+
+ )}
+ {agreeToRules && }
+
+
+
+ >
+ )
+}
diff --git a/src/components/Wrapper/UnauthenticatedUser.js b/src/components/Wrapper/UnauthenticatedUser.js
new file mode 100644
index 0000000..5790d20
--- /dev/null
+++ b/src/components/Wrapper/UnauthenticatedUser.js
@@ -0,0 +1,42 @@
+import Header from '../Header'
+import Navbar from '../Navbar'
+import { Flex, Box, Heading, Text } from '@chakra-ui/react'
+import { signIn } from 'next-auth/react'
+import DiscordButton from '../DiscordButton'
+
+export default function UnauthenticatedUser() {
+ return (
+ <>
+
+
+
+
+
+
+ Sign In
+
+
+ signIn('discord')} />
+
+
+
+ Are you a new user? Start by clicking the button
+ above, and we will help you with creating your
+ profile.
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/components/Wrapper/index.js b/src/components/Wrapper/index.js
new file mode 100644
index 0000000..813bbec
--- /dev/null
+++ b/src/components/Wrapper/index.js
@@ -0,0 +1,28 @@
+import NewUser from './NewUser'
+import ExistingUser from './ExistingUser'
+import UnauthenticatedUser from './UnauthenticatedUser'
+import Loading from './Loading'
+
+function Wrapper({ children, session, status }) {
+ // Loading State
+ if (status === 'loading') {
+ return
+ }
+
+ if (status === 'unauthenticated') {
+ return
+ }
+
+ // If user is authenticated and exists in database
+ if (status === 'authenticated' && session.dbUser) {
+ const isBanned = session.dbUser.isBanned
+ return {children}
+ }
+
+ // If user is authenticated and doesn't exist in database then force creating a profile
+ if (status !== 'loading' && status === 'authenticated' && !session.dbUser) {
+ return
+ }
+}
+
+export default Wrapper
diff --git a/src/components/util.js b/src/components/util.js
new file mode 100644
index 0000000..f4b65fb
--- /dev/null
+++ b/src/components/util.js
@@ -0,0 +1,104 @@
+import { DateTime } from 'luxon'
+
+export function range(start, end) {
+ let arr = []
+ for (let i = start; i <= end; i++) {
+ arr.push(i)
+ }
+ return arr
+}
+
+export function getNumberOfProjectsRequested(projects, session) {
+ const requestedMembers = projects?.map(
+ (project) => project?.requestedMembers
+ )
+
+ let counter = 0
+
+ for (let i = 0; i < requestedMembers?.length; i++) {
+ for (let j = 0; j < requestedMembers[i]?.length; j++) {
+ if (requestedMembers[i][j]._id === session?.dbUser?._id) {
+ counter++
+ }
+ }
+ }
+
+ return counter
+}
+
+export function getRelativeProjectDates(project) {
+ const currentDate = DateTime.now()
+
+ const creationDate = DateTime.fromISO(project?.createdAt)
+ const creationDifference = creationDate?.diff(currentDate, [
+ 'days',
+ 'hours',
+ 'minutes',
+ ])
+
+ const durationSinceCreation = creationDifference?.values
+
+ const expirationDate = DateTime.fromISO(project?.expiresIn)
+ const expirationDifference = expirationDate?.diff(currentDate, [
+ 'days',
+ 'hours',
+ 'minutes',
+ ])
+
+ const durationTillExpiration = expirationDifference?.values
+
+ return { durationTillExpiration, durationSinceCreation }
+}
+
+export function formatRelativeProjectDates({
+ durationTillExpiration: expiry,
+ durationSinceCreation: create,
+}) {
+ let expiresMessage = ''
+ let createdMessage = ''
+
+ // Expiration
+ if (expiry?.days > 0) {
+ const daysTill = Math.floor(Math.abs(expiry?.days))
+ if (expiry?.days == 1) {
+ expiresMessage = `Expires in ${daysTill} day`
+ }
+
+ if (expiry?.days > 1) {
+ expiresMessage = `Expires in ${daysTill} days`
+ }
+ } else {
+ if (expiry?.hours === 0) {
+ const minutesTill = Math.floor(Math.abs(expiry?.minutes))
+ if (expiry?.minutes <= 1) {
+ expiresMessage = `Expires in ${minutesTill} minute`
+ }
+ if (expiry?.minutes > 1) {
+ expiresMessage = `Expires in ${minutesTill} minutes`
+ }
+ } else {
+ const hoursTill = Math.floor(Math.abs(expiry?.hours))
+ const minutesTill = Math.floor(Math.abs(expiry?.minutes))
+ expiresMessage = `Expires in ${hoursTill} ${
+ hoursTill == 1 ? 'hour' : 'hours'
+ } and ${minutesTill} ${minutesTill == 1 ? 'minute' : 'minutes'}`
+ }
+ }
+
+ // Creation
+ if (create?.days === 0) {
+ if (create?.hours === 0) {
+ const minutesAgo = Math.floor(Math.abs(create?.minutes))
+ createdMessage = `Posted ${minutesAgo} minutes ago`
+ } else {
+ const hoursAgo = Math.floor(Math.abs(create?.hours))
+ const minutesAgo = Math.floor(Math.abs(create?.minutes))
+ createdMessage = `Posted ${hoursAgo} hours and ${minutesAgo} minutes ago`
+ }
+ } else {
+ const daysAgo = Math.floor(Math.abs(create?.days))
+ createdMessage = `Posted ${daysAgo} days ago`
+ }
+
+ return { expiresMessage, createdMessage }
+}
diff --git a/src/controllers/project.js b/src/controllers/project.js
new file mode 100644
index 0000000..f81e0d1
--- /dev/null
+++ b/src/controllers/project.js
@@ -0,0 +1,69 @@
+export async function getProjects(cookie, searchQuery = '') {
+ try {
+ let apiEndpoint
+ if (searchQuery == '') {
+ apiEndpoint = `${process.env.NEXT_PUBLIC_DOMAIN}/api/projects`
+ } else {
+ apiEndpoint = `${process.env.NEXT_PUBLIC_DOMAIN}/api/projects?title=${searchQuery}`
+ }
+
+ const response = await fetch(apiEndpoint, {
+ method: 'GET',
+ headers: {
+ Cookie: cookie,
+ },
+ })
+ return await response.json()
+ } catch (err) {
+ return err
+ }
+}
+
+export async function getProjectById(context) {
+ try {
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_DOMAIN}/api/projects/${context.params.id}`,
+ {
+ method: 'GET',
+ headers: {
+ Cookie: context.req.headers.cookie,
+ },
+ }
+ )
+ return await response.json()
+ } catch (err) {
+ return err
+ }
+}
+
+export async function deleteProjectIdea(id) {
+ try {
+ const response = await fetch(`/api/projects/${id}`, {
+ method: 'DELETE',
+ headers: { 'Content-Type': 'application/json' },
+ })
+ if (!response.ok) {
+ return false
+ }
+ return true
+ } catch (error) {
+ return false
+ }
+}
+
+export async function patchProject(id, formDataProject) {
+ try {
+ const response = await fetch(`/api/projects/${id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(formDataProject),
+ })
+
+ if (!response.ok) {
+ return false
+ }
+ return true
+ } catch (error) {
+ return false
+ }
+}
diff --git a/src/models/project.js b/src/models/project.js
new file mode 100644
index 0000000..0cfed0f
--- /dev/null
+++ b/src/models/project.js
@@ -0,0 +1,52 @@
+const { Schema, model, models } = require('mongoose')
+
+const projectSchema = new Schema({
+ title: {
+ type: String,
+ required: true,
+ unique: true,
+ },
+ technologies: [
+ {
+ type: String,
+ required: true,
+ },
+ ],
+ details: {
+ type: String,
+ required: true,
+ },
+ timezone: {
+ type: String,
+ required: true,
+ },
+ createdAt: {
+ type: Date,
+ required: true,
+ },
+ expiresIn: {
+ type: Date,
+ required: true,
+ },
+ admin: {
+ required: true,
+ type: Schema.Types.ObjectId,
+ ref: 'User',
+ },
+ currentMembers: [
+ {
+ type: Schema.Types.ObjectId,
+ ref: 'User',
+ },
+ ],
+ requestedMembers: [
+ {
+ type: Schema.Types.ObjectId,
+ ref: 'User',
+ },
+ ],
+})
+
+const Project = models.Project || model('Project', projectSchema)
+
+export default Project
diff --git a/src/models/user.js b/src/models/user.js
new file mode 100644
index 0000000..8cfb3a2
--- /dev/null
+++ b/src/models/user.js
@@ -0,0 +1,36 @@
+const { Schema, model, models } = require('mongoose')
+
+const userSchema = new Schema({
+ username: {
+ type: String,
+ required: true,
+ unique: true,
+ },
+ authenticatedDiscordId: {
+ type: String,
+ required: true,
+ },
+ discordUsername: {
+ type: String,
+ required: true,
+ },
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ },
+ location: {
+ type: String,
+ },
+ githubLink: String,
+ preferredMethodOfContact: String,
+ discordAvatarUrl: String,
+ isBanned: {
+ value: { type: Boolean },
+ reason: { type: String },
+ },
+})
+
+const User = models.User || model('User', userSchema)
+
+export default User
diff --git a/src/pages/_app.js b/src/pages/_app.js
new file mode 100644
index 0000000..5369c10
--- /dev/null
+++ b/src/pages/_app.js
@@ -0,0 +1,16 @@
+import '../../styles/globals.css'
+import { ChakraProvider } from '@chakra-ui/react'
+import { SessionProvider } from 'next-auth/react'
+
+export default function App({
+ Component,
+ pageProps: { session, ...pageProps },
+}) {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js
new file mode 100644
index 0000000..ef1177b
--- /dev/null
+++ b/src/pages/api/auth/[...nextauth].js
@@ -0,0 +1,68 @@
+import NextAuth from 'next-auth'
+import DiscordProvider from 'next-auth/providers/discord'
+import User from '../../../models/user'
+import connectToDatabase from '../../../utils/dbConnect'
+connectToDatabase()
+
+export const authOptions = {
+ // Configure one or more authentication providers
+ providers: [
+ DiscordProvider({
+ clientId: process.env.DISCORD_CLIENT_ID,
+ clientSecret: process.env.DISCORD_CLIENT_SECRET,
+ authorization: { params: { scope: 'identify guilds email' } },
+ profile(profile) {
+ if (profile.avatar === null) {
+ const defaultAvatarNumber =
+ parseInt(profile.discriminator) % 5
+ profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`
+ } else {
+ const format = profile.avatar.startsWith('a_')
+ ? 'gif'
+ : 'png'
+ profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`
+ }
+ return {
+ id: profile.id,
+ name: profile.username,
+ email: profile.email,
+ image: profile.image_url,
+ }
+ },
+ }),
+ ],
+ callbacks: {
+ async jwt({ token, account }) {
+ // Persist the OAuth access_token to the token right after signin
+ if (account) {
+ token.userId = account.providerAccountId
+ }
+ return token
+ },
+ async session({ session, token, user, profile }) {
+ // Send properties to the client, like an access_token from a provider.
+ // session.accessToken = token.accessToken
+ let dbUser = await User.findOne({
+ authenticatedDiscordId: token.sub,
+ })
+ session.dbUser = dbUser
+ session.userId = token.sub
+
+ return session
+ },
+ async signIn({ user, account, profile, email, credentials }) {
+ // console.log(user, account, profile, email, credentials)
+ // implement database lookups below
+ const isAllowedToSignIn = true
+ if (isAllowedToSignIn) {
+ return true
+ } else {
+ // Return false to display a default error message
+ return false
+ // Or you can return a URL to redirect to:
+ // return '/unauthorized'
+ }
+ },
+ },
+}
+export default NextAuth(authOptions)
diff --git a/src/pages/api/projects/[id].js b/src/pages/api/projects/[id].js
new file mode 100644
index 0000000..d2b2b1a
--- /dev/null
+++ b/src/pages/api/projects/[id].js
@@ -0,0 +1,128 @@
+import connectToDatabase from '../../../utils/dbConnect'
+import Project from '../../../models/project'
+import { unstable_getServerSession } from 'next-auth/next'
+import { authOptions } from '../auth/[...nextauth]'
+
+async function countProjects(userId) {
+ const joinLimit = process.env.NEXT_PUBLIC_JOINLIMIT
+ const count = await Project.find({ requestedMembers: userId }).count()
+ console.log(count)
+ if (count < joinLimit) {
+ return true
+ }
+ return false
+}
+
+export default async function handler(req, res) {
+ const session = await unstable_getServerSession(req, res, authOptions)
+ if (session) {
+ const { method } = req
+ connectToDatabase()
+ switch (method) {
+ case 'GET':
+ try {
+ const project = await Project.findById(req.query.id)
+ .populate('admin')
+ .populate('currentMembers')
+ .populate('requestedMembers')
+ return res.status(200).json(project)
+ } catch (err) {
+ return res
+ .status(500)
+ .json({ message: 'Project Not found' })
+ }
+ break
+ case 'PATCH':
+ const { user_id, requestType } = req.body
+ const { id } = req.query
+ const options = {
+ new: true, // THis option is to return updated in same update request
+ }
+ try {
+ if (requestType == 'requestForProject') {
+ if (!(await countProjects(user_id))) {
+ return res.status(400).send({
+ error: 'You reached your project join limit',
+ })
+ }
+ const project = await Project.findById(id)
+ const exists =
+ project.requestedMembers.includes(user_id)
+ if (!exists) {
+ const project = await Project.findByIdAndUpdate(
+ id,
+ { $push: { requestedMembers: user_id } },
+ options
+ )
+ return res.status(200).json(project)
+ } else {
+ return res.status(400).json({
+ error: 'You already requested for this project',
+ })
+ }
+ } else if (
+ requestType == 'rejectProject' ||
+ requestType == 'withdrawFromProject'
+ ) {
+ const project = await Project.findByIdAndUpdate(
+ id,
+ { $pull: { requestedMembers: user_id } },
+ options
+ )
+ return res.status(200).json(project)
+ } else if (requestType == 'approveProject') {
+ const project = await Project.findById(id)
+ const exists = project.currentMembers.includes(user_id)
+ if (!exists) {
+ await Project.findByIdAndUpdate(
+ id,
+ { $pull: { requestedMembers: user_id } },
+ options
+ )
+ const project = await Project.findByIdAndUpdate(
+ id,
+ { $push: { currentMembers: user_id } },
+ options
+ )
+ return res.status(200).json(project)
+ } else {
+ return res.status(400).json({
+ error: 'You already accepted this member',
+ })
+ }
+ } else {
+ return res.status(400).json('requestType is invalid')
+ }
+ } catch (err) {
+ return res.status(500).json(err)
+ }
+ break
+ case 'DELETE':
+ try {
+ const project = await Project.findById(req.query.id)
+ if (
+ JSON.stringify(session.dbUser._id) ===
+ JSON.stringify(project.admin._id)
+ ) {
+ await Project.findByIdAndDelete(req.query.id)
+ return res.status(200).json({
+ success: 'You successfuly deleted your project',
+ })
+ } else {
+ return res.status(401).json({
+ error: 'Sorry you are not authorised to delete this project',
+ })
+ }
+ } catch (err) {
+ return res.status(500).json(err)
+ }
+ break
+ default:
+ return res
+ .status(400)
+ .json({ message: 'Method type not supported' })
+ }
+ } else {
+ return res.status(401).send({ error: 'You need to sign in first' })
+ }
+}
diff --git a/src/pages/api/projects/index.js b/src/pages/api/projects/index.js
new file mode 100644
index 0000000..981a6aa
--- /dev/null
+++ b/src/pages/api/projects/index.js
@@ -0,0 +1,98 @@
+import connectToDatabase from '../../../utils/dbConnect'
+import Project from '../../../models/project'
+import { DateTime } from 'luxon'
+import {
+ validateProjectBody,
+ existingProjectTitle,
+} from '../../../utils/validation'
+import { unstable_getServerSession } from 'next-auth/next'
+import { authOptions } from '../auth/[...nextauth]'
+
+async function countProjects(admin) {
+ const postLimit = process.env.NEXT_PUBLIC_POSTLIMIT
+ const count = await Project.where({ admin: admin }).count()
+ if (count < postLimit) {
+ return true
+ }
+ return false
+}
+
+export default async function handler(req, res) {
+ const session = await unstable_getServerSession(req, res, authOptions)
+ if (session) {
+ const { method } = req
+ connectToDatabase()
+
+ switch (method) {
+ case 'GET':
+ try {
+ const { title } = req.query
+ let projects
+ if (title) {
+ projects = await Project.find({
+ title: new RegExp(title, 'i'),
+ })
+ .populate('admin')
+ .populate('currentMembers')
+ .populate('requestedMembers')
+ } else {
+ projects = await Project.find()
+ .populate('admin')
+ .populate('currentMembers')
+ .populate('requestedMembers')
+ }
+ return res.status(200).json(projects)
+ } catch (err) {
+ return res
+ .status(500)
+ .json({ message: 'Projects Not found' })
+ }
+ break
+ case 'POST':
+ const { title, technologies, details, admin, timezone } =
+ req.body
+ const validationResponse = validateProjectBody(
+ title,
+ technologies,
+ details,
+ admin,
+ timezone
+ )
+
+ if (!(await countProjects(admin))) {
+ return res
+ .status(400)
+ .send({ error: 'You reached your project post limit' })
+ }
+
+ if (validationResponse != true) {
+ return res.status(400).send({ error: validationResponse })
+ }
+ try {
+ const existingProjectTitleResponse =
+ await existingProjectTitle(title)
+ if (existingProjectTitleResponse != true) {
+ return res
+ .status(400)
+ .json({ error: existingProjectTitleResponse })
+ }
+ const project = new Project(req.body)
+ const now = DateTime.now()
+ project.createdAt = now
+ project.expiresIn = now.plus({ week: 1 })
+ project.admin = admin
+ await project.save()
+ return res.status(200).json(project)
+ } catch (err) {
+ return res.status(500).json(err)
+ }
+ break
+ default:
+ return res
+ .status(400)
+ .json({ message: 'Method type not supported' })
+ }
+ } else {
+ return res.status(401).send({ error: 'You need to sign in first' })
+ }
+}
diff --git a/src/pages/api/user/[id].js b/src/pages/api/user/[id].js
new file mode 100644
index 0000000..56fcd96
--- /dev/null
+++ b/src/pages/api/user/[id].js
@@ -0,0 +1,36 @@
+//GET user and Delete User Dynamic routes are not in use currently.
+//Make sure to protect this route before using this
+
+// import connectToDatabase from '../../../utils/dbConnect'
+// import User from '../../../models/user'
+
+// export default async function handler(req, res) {
+// const { method } = req
+// connectToDatabase()
+
+// switch (method) {
+// case 'GET':
+// try {
+// const user = await User.findById(req.query.id)
+// return res.status(200).json(user)
+// } catch (err) {
+// return res.status(500).json({ message: 'User Not found' })
+// }
+// break
+// case 'PATCH':
+// // Patch case for future use. No need for this phase of project
+// break
+// case 'DELETE':
+// try {
+// const user = await User.findByIdAndDelete(req.query.id)
+// return res.status(200).json(user)
+// } catch (err) {
+// return res.status(500).json(err)
+// }
+// break
+// default:
+// return res
+// .status(400)
+// .json({ message: 'Method type not supported' })
+// }
+// }
diff --git a/src/pages/api/user/index.js b/src/pages/api/user/index.js
new file mode 100644
index 0000000..a9fce71
--- /dev/null
+++ b/src/pages/api/user/index.js
@@ -0,0 +1,77 @@
+import connectToDatabase from '../../../utils/dbConnect'
+import User from '../../../models/user'
+import { validateUserBody, existingUser } from '../../../utils/validation'
+import { unstable_getServerSession } from 'next-auth/next'
+import { authOptions } from '../auth/[...nextauth]'
+
+export default async function handler(req, res) {
+ const session = await unstable_getServerSession(req, res, authOptions)
+ if (session) {
+ const { method } = req
+ connectToDatabase()
+
+ switch (method) {
+ // GET all users method is currently not in USE. It may require in future to give access to Chingu admin
+ // Make sure to protect this route before using this
+ // case 'GET':
+ // try {
+ // const users = await User.find()
+ // return res.status(200).json(users)
+ // } catch (err) {
+ // return res.status(500).json({ message: 'Users Not found' })
+ // }
+ // break
+ case 'POST':
+ const {
+ username,
+ location,
+ email,
+ authenticatedDiscordId,
+ discordUsername,
+ preferredMethodOfContact,
+ githubLink,
+ } = req.body
+
+ const validationResponse = validateUserBody(
+ username,
+ location,
+ email,
+ authenticatedDiscordId,
+ discordUsername,
+ preferredMethodOfContact,
+ githubLink
+ )
+
+ if (validationResponse != true) {
+ return res.status(400).send({ error: validationResponse })
+ }
+
+ try {
+ const existingUserResponse = await existingUser(
+ username,
+ email
+ )
+ if (existingUserResponse != true) {
+ return res
+ .status(400)
+ .json({ error: existingUserResponse })
+ }
+ const user = new User(req.body)
+ await user.save()
+ return res.status(200).json({ success: 'User Created' })
+ } catch (err) {
+ return res.status(500).json(err)
+ }
+ break
+ case 'PATCH':
+ // Patch case for future use. No need for this phase of project
+ break
+ default:
+ return res
+ .status(400)
+ .json({ message: 'Method type not supported' })
+ }
+ } else {
+ return res.status(401).send({ error: 'You need to sign in first' })
+ }
+}
diff --git a/src/pages/index.js b/src/pages/index.js
new file mode 100644
index 0000000..9272607
--- /dev/null
+++ b/src/pages/index.js
@@ -0,0 +1,35 @@
+import { useEffect } from 'react'
+import { useSession } from 'next-auth/react'
+import { useRouter } from 'next/router'
+import Wrapper from '../components/Wrapper'
+import { Heading, Flex, Progress, Stack } from '@chakra-ui/react'
+
+export default function Home() {
+ const { data: session, status } = useSession()
+ const router = useRouter()
+
+ useEffect(() => {
+ if (session?.dbUser) {
+ router.push('/projects')
+ }
+ }, [session, router])
+
+ return (
+
+
+
+ Redirecting
+
+
+
+
+
+
+ )
+}
diff --git a/src/pages/projects/[id].js b/src/pages/projects/[id].js
new file mode 100644
index 0000000..341f503
--- /dev/null
+++ b/src/pages/projects/[id].js
@@ -0,0 +1,78 @@
+import ManageProject from '../../components/ManageProject'
+import ProjectDetails from '../../components/ProjectDetails'
+import { useSession } from 'next-auth/react'
+import { authOptions } from '../api/auth/[...nextauth]'
+import { unstable_getServerSession } from 'next-auth'
+import Wrapper from '../../components/Wrapper'
+import { getNumberOfProjectsRequested } from '../../components/util.js'
+import { getProjects, getProjectById } from '../../controllers/project'
+
+export default function Project({
+ details,
+ projects,
+ isRequestedMember,
+ isCurrentMember,
+}) {
+ const JOINLIMIT = process.env.NEXT_PUBLIC_JOINLIMIT
+
+ const { data: session, status } = useSession()
+
+ const projectExists = Object.keys(details).length > 0
+
+ const isAdmin = session?.dbUser?._id === details?.admin?._id
+
+ const projectsRequested = getNumberOfProjectsRequested(projects, session)
+
+ const isRequestable = projectsRequested < JOINLIMIT
+
+ const isJoinable = !isRequestedMember && isRequestable && !isCurrentMember
+
+ return (
+
+ {projectExists && isAdmin ? (
+
+ ) : projectExists && !isAdmin ? (
+
+ ) : (
+ 'The project you are looking for does not exist.'
+ )}
+
+ )
+}
+
+export const getServerSideProps = async (context) => {
+ try {
+ const session = await unstable_getServerSession(
+ context.req,
+ context.res,
+ authOptions
+ )
+
+ const projectData = await getProjectById(context)
+
+ const allProjectsData = await getProjects(context.req.headers.cookie)
+
+ const requestedMembers = projectData?.requestedMembers.map(
+ (member) => member._id
+ )
+
+ const currentMembers = projectData?.currentMembers.map(
+ (member) => member._id
+ )
+ const authenticatedUserId = session?.dbUser?._id.toString()
+
+ const isRequestedMember = requestedMembers.includes(authenticatedUserId)
+ const isCurrentMember = currentMembers.includes(authenticatedUserId)
+
+ return {
+ props: {
+ details: projectData,
+ projects: allProjectsData,
+ isRequestedMember,
+ isCurrentMember,
+ },
+ }
+ } catch (error) {
+ return { props: { details: {}, projects: [] } }
+ }
+}
diff --git a/src/pages/projects/index.js b/src/pages/projects/index.js
new file mode 100644
index 0000000..f5ad386
--- /dev/null
+++ b/src/pages/projects/index.js
@@ -0,0 +1,294 @@
+import LimitsOverview from '../../components/LimitsOverview'
+import ProjectPreviewCard from '../../components/ProjectPreviewCard'
+import ProjectActions from '../../components/ProjectActions'
+import {
+ Flex,
+ Text,
+ HStack,
+ VStack,
+ useMediaQuery,
+ list,
+} from '@chakra-ui/react'
+import { useSession } from 'next-auth/react'
+import Wrapper from '../../components/Wrapper'
+import DetailsPreviewCard from '../../components/DetailsPreviewCard'
+import { useEffect, useState, useReducer } from 'react'
+import { authOptions } from '../api/auth/[...nextauth]'
+import { unstable_getServerSession } from 'next-auth'
+import { getProjects } from '../../controllers/project'
+import { getNumberOfProjectsRequested } from '../../components/util'
+import Pagination from '../../components/Pagination'
+import { range } from '../../components/util'
+
+export default function Projects({
+ projects, //projects concats the order of authenticatedProjects followed by otherProjects
+ authenticatedProjects,
+ otherProjects,
+ cookie,
+}) {
+ const CREATELIMIT = process.env.NEXT_PUBLIC_POSTLIMIT
+ const [selectedProject, setSelectedProject] = useState({})
+ const [isLargerThan768] = useMediaQuery('(min-width: 768px)')
+ const [filteredProjects, setFilteredProjects] = useState(false)
+ const { data: session, status } = useSession()
+
+ const listOfProjects =
+ filteredProjects !== false ? filteredProjects : projects
+
+ const selectedProjectHandler = (project) => {
+ if (isLargerThan768) {
+ setSelectedProject(project)
+ }
+ return
+ }
+
+ const countOfProjectsRequested = getNumberOfProjectsRequested(
+ otherProjects,
+ session
+ )
+
+ const searchHandler = async (value) => {
+ if (value.trim().length > 0) {
+ const projects = await getProjects(cookie, value)
+ setFilteredProjects(projects)
+ } else {
+ //reset state
+ setFilteredProjects(false)
+ }
+ }
+
+ // Pagination Logic
+ const PROJECTSPERPAGE = 5
+ const TOTALPROJECTPAGES = Math.ceil(listOfProjects.length / PROJECTSPERPAGE)
+ const MAXNUMBEROFPAGESTOSHOW = 3 //Excludes arrow buttons
+
+ const projectsOnPage = (projects, selectedPage) => {
+ return projects.slice(
+ Number(PROJECTSPERPAGE * selectedPage) - PROJECTSPERPAGE,
+ Number(PROJECTSPERPAGE * selectedPage)
+ )
+ }
+
+ const initialMaxPage =
+ TOTALPROJECTPAGES <= MAXNUMBEROFPAGESTOSHOW
+ ? TOTALPROJECTPAGES
+ : MAXNUMBEROFPAGESTOSHOW
+
+ const initialState = {
+ selectedPage: 1,
+ minPage: 1,
+ maxPage: initialMaxPage,
+ pageRange: range(1, initialMaxPage),
+ }
+
+ function reducer(state, action) {
+ switch (action.type) {
+ case 'MOUNT':
+ setSelectedProject(projectsOnPage(listOfProjects, 1)[0])
+ if (TOTALPROJECTPAGES <= MAXNUMBEROFPAGESTOSHOW) {
+ return {
+ selectedPage: 1,
+ minPage: 1,
+ maxPage: TOTALPROJECTPAGES,
+ pageRange: range(state.minPage, TOTALPROJECTPAGES),
+ }
+ }
+ if (TOTALPROJECTPAGES > MAXNUMBEROFPAGESTOSHOW) {
+ return {
+ selectedPage: 1,
+ minPage: 1,
+ maxPage: MAXNUMBEROFPAGESTOSHOW,
+ pageRange: range(state.minPage, MAXNUMBEROFPAGESTOSHOW),
+ }
+ }
+ break
+ case 'CHANGE':
+ const newProjectsOnPage = projectsOnPage(
+ listOfProjects,
+ Number(action.payload.selectedPage)
+ )
+ setSelectedProject(newProjectsOnPage[0])
+ if (action.payload.selectedPage > state.maxPage) {
+ const difference =
+ TOTALPROJECTPAGES - Number(action.payload.selectedPage)
+ return {
+ selectedPage: Number(action.payload.selectedPage),
+ minPage: Number(action.payload.selectedPage),
+ maxPage:
+ difference < 2
+ ? TOTALPROJECTPAGES
+ : Number(action.payload.selectedPage) + 2,
+ pageRange: range(
+ Number(action.payload.selectedPage),
+ difference < 2
+ ? TOTALPROJECTPAGES
+ : Number(action.payload.selectedPage) + 2
+ ),
+ }
+ }
+
+ // selected page is between the min and max pages
+ if (
+ action.payload.selectedPage >= state.minPage &&
+ action.payload.selectedPage <= state.maxPage
+ ) {
+ return {
+ selectedPage: Number(action.payload.selectedPage),
+ minPage: state.minPage,
+ maxPage: state.maxPage,
+ pageRange: range(state.minPage, state.maxPage),
+ }
+ }
+
+ // selected page is below min page
+ if (action.payload.selectedPage < state.minPage) {
+ const difference = Number(action.payload.selectedPage) - 2
+ return {
+ selectedPage: Number(action.payload.selectedPage),
+ minPage: difference > 0 ? difference : 1,
+ maxPage: Number(action.payload.selectedPage),
+ pageRange: range(
+ difference > 0 ? difference : 1,
+ Number(action.payload.selectedPage)
+ ),
+ }
+ }
+ break
+ default:
+ console.log('Enter a valid action.')
+ }
+ }
+ const [page, dispatch] = useReducer(reducer, initialState)
+
+ const changeProjectPageHandler = (page) => {
+ dispatch({ type: 'CHANGE', payload: { selectedPage: page } })
+ }
+
+ useEffect(() => {
+ dispatch({ type: 'MOUNT' })
+ }, [listOfProjects])
+
+ return (
+
+
+
+ = CREATELIMIT
+ }
+ onSearch={(query) => searchHandler(query)}
+ />
+
+
+
+ {`${listOfProjects.length} projects ${
+ filteredProjects !== false ? 'found' : 'posted'
+ }.`}
+
+
+
+
+ {projectsOnPage(
+ listOfProjects,
+ page?.selectedPage
+ ).map((project) => {
+ return (
+
+ selectedProjectHandler(project)
+ }
+ externalDetails={!isLargerThan768}
+ key={project._id}
+ project={project}
+ isSelected={
+ project._id == selectedProject?._id
+ ? true
+ : false
+ }
+ />
+ )
+ })}
+
+ {listOfProjects.length > 0 && (
+
+
+
+ )}
+
+
+ changeProjectPageHandler(page)}
+ />
+
+
+ )
+}
+
+export const getServerSideProps = async (context) => {
+ const session = await unstable_getServerSession(
+ context.req,
+ context.res,
+ authOptions
+ )
+
+ const adminId = session?.dbUser?._id.toString()
+ try {
+ const data = await getProjects(context.req.headers.cookie)
+
+ const authenticatedProjects = data.filter(
+ (project) => project.admin._id === adminId
+ )
+ const otherProjects = data.filter(
+ (project) => project.admin._id !== adminId
+ )
+
+ const projects = authenticatedProjects.concat(otherProjects)
+ return {
+ props: {
+ projects,
+ authenticatedProjects,
+ otherProjects,
+ cookie: context.req.headers.cookie,
+ },
+ }
+ } catch (error) {
+ return {
+ props: {
+ projects: [],
+ authenticatedProjects: [],
+ otherProjects: [],
+ },
+ }
+ }
+}
diff --git a/src/utils/dbConnect.js b/src/utils/dbConnect.js
new file mode 100644
index 0000000..0e0c0a4
--- /dev/null
+++ b/src/utils/dbConnect.js
@@ -0,0 +1,12 @@
+import mongoose from "mongoose";
+
+const connectToDatabase = async () => {
+ try {
+ mongoose.connect(process.env.MONGODB_URI);
+ }
+ catch (err) {
+ console.log(err)
+ }
+}
+
+export default connectToDatabase;
\ No newline at end of file
diff --git a/src/utils/validation.js b/src/utils/validation.js
new file mode 100644
index 0000000..d27d3f2
--- /dev/null
+++ b/src/utils/validation.js
@@ -0,0 +1,114 @@
+import User from '../models/user'
+import Project from '../models/project'
+
+export function validateProjectBody(
+ title,
+ technologies,
+ details,
+ admin,
+ timezone
+) {
+ if (title == undefined || title == '') {
+ return 'Title parameter is required'
+ } else if (technologies == undefined || technologies == '') {
+ return 'Technologies parameter is required'
+ } else if (details == undefined || details == '') {
+ return 'Details parameter is required'
+ } else if (admin == undefined || admin == '') {
+ return 'admin parameter is required'
+ } else if (timezone == undefined || timezone == '') {
+ return 'timezone parameter is required'
+ } else if (typeof title !== 'string') {
+ return 'Title parameter should be string'
+ } else if (title.length < 5 || title.length > 51) {
+ return 'Title parameter length should be between 5 to 20 characters'
+ } else if (technologies.length < 1) {
+ return 'At least one technology should be selected'
+ } else if (details.length < 250 || details.length > 800) {
+ return 'Description should be between 250 to 800 characters'
+ } else {
+ return true
+ }
+}
+
+export function validateUserBody(
+ username,
+ location,
+ email,
+ authenticatedDiscordId,
+ discordUsername,
+ preferredMethodOfContact,
+ githubLink
+) {
+ if (location == undefined) {
+ location = ''
+ }
+ if (githubLink == undefined) {
+ githubLink = ''
+ }
+ if (username == undefined || username == '') {
+ return 'username parameter is required'
+ } else if (typeof username !== 'string') {
+ return 'username parameter should be string'
+ } else if (username.length < 4 || username.length > 17) {
+ return 'username parameter length should be between 4 to 17'
+ } else if (email == undefined || email == '') {
+ return 'email parameter is required'
+ } else if (!email.includes('@')) {
+ return 'email format is invalid'
+ } else if (
+ authenticatedDiscordId == undefined ||
+ authenticatedDiscordId == ''
+ ) {
+ return 'authenticatedDiscordId is required'
+ } else if (discordUsername == undefined || discordUsername == '') {
+ return 'discordUsername is required'
+ } else if (!discordUsername.includes('#')) {
+ return 'discordUsername format is invalid'
+ } else if (
+ preferredMethodOfContact == undefined ||
+ preferredMethodOfContact == ''
+ ) {
+ return 'preferredMethodOfContact parameter is required'
+ } else {
+ if (location != '') {
+ if (!location.includes(',')) {
+ return 'location format is invalid'
+ }
+ }
+ if (githubLink != '') {
+ if (!githubLink.includes('https://github.com/')) {
+ return 'Github link format is invalid'
+ }
+ }
+ return true
+ }
+}
+
+export async function existingUser(username, email) {
+ const existingUsername = await User.findOne({
+ username: username,
+ })
+ const existingEmail = await User.findOne({ email: email })
+ if (existingUsername != null && existingEmail != null) {
+ return 'Both username and email is taken'
+ }
+ if (existingUsername != null) {
+ return 'Username is already taken'
+ } else if (existingEmail != null) {
+ return 'This email is already link to another account'
+ } else {
+ return true
+ }
+}
+
+export async function existingProjectTitle(title) {
+ const existingProjectTitle = await Project.findOne({
+ title: title,
+ })
+ if (existingProjectTitle != null) {
+ return 'Project Title is already taken'
+ } else {
+ return true
+ }
+}
diff --git a/styles/Home.module.css b/styles/Home.module.css
deleted file mode 100644
index bd50f42..0000000
--- a/styles/Home.module.css
+++ /dev/null
@@ -1,129 +0,0 @@
-.container {
- padding: 0 2rem;
-}
-
-.main {
- min-height: 100vh;
- padding: 4rem 0;
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
-}
-
-.footer {
- display: flex;
- flex: 1;
- padding: 2rem 0;
- border-top: 1px solid #eaeaea;
- justify-content: center;
- align-items: center;
-}
-
-.footer a {
- display: flex;
- justify-content: center;
- align-items: center;
- flex-grow: 1;
-}
-
-.title a {
- color: #0070f3;
- text-decoration: none;
-}
-
-.title a:hover,
-.title a:focus,
-.title a:active {
- text-decoration: underline;
-}
-
-.title {
- margin: 0;
- line-height: 1.15;
- font-size: 4rem;
-}
-
-.title,
-.description {
- text-align: center;
-}
-
-.description {
- margin: 4rem 0;
- line-height: 1.5;
- font-size: 1.5rem;
-}
-
-.code {
- background: #fafafa;
- border-radius: 5px;
- padding: 0.75rem;
- font-size: 1.1rem;
- font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
- Bitstream Vera Sans Mono, Courier New, monospace;
-}
-
-.grid {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-wrap: wrap;
- max-width: 800px;
-}
-
-.card {
- margin: 1rem;
- padding: 1.5rem;
- text-align: left;
- color: inherit;
- text-decoration: none;
- border: 1px solid #eaeaea;
- border-radius: 10px;
- transition: color 0.15s ease, border-color 0.15s ease;
- max-width: 300px;
-}
-
-.card:hover,
-.card:focus,
-.card:active {
- color: #0070f3;
- border-color: #0070f3;
-}
-
-.card h2 {
- margin: 0 0 1rem 0;
- font-size: 1.5rem;
-}
-
-.card p {
- margin: 0;
- font-size: 1.25rem;
- line-height: 1.5;
-}
-
-.logo {
- height: 1em;
- margin-left: 0.5rem;
-}
-
-@media (max-width: 600px) {
- .grid {
- width: 100%;
- flex-direction: column;
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .card,
- .footer {
- border-color: #222;
- }
- .code {
- background: #111;
- }
- .logo img {
- filter: invert(1);
- }
-}
diff --git a/styles/globals.css b/styles/globals.css
index 0320094..6a74936 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -1,26 +1,37 @@
* {
- box-sizing: border-box;
+ box-sizing: border-box;
}
html,
body {
- padding: 0;
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
- Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ padding: 0;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
-a {
- color: inherit;
- text-decoration: none;
+#__next {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-width: 320px;
}
-@media (prefers-color-scheme: dark) {
- html {
- color-scheme: dark;
- }
- body {
- color: white;
- background: black;
- }
+.container {
+ min-width: 320px;
+ width: 100vw;
+ max-width: 1400px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.content {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
}