From 392235a4606eab8ed635ff0e1f286c7dd8899dde Mon Sep 17 00:00:00 2001 From: fabien-h Date: Thu, 31 Dec 2020 16:40:14 +0100 Subject: [PATCH] add code --- modulesDeclarationPCSS.d.ts | 7 + modulesDeclarationSVG.d.ts | 4 + .babelrc | 3 + .eslintignore | 3 + .eslintrc.json | 100 + .gitignore | 32 +- .prettierignore | 5 + .prettierrc | 11 + .stylelintignore | 1 + .vscode/extensions.json | 8 + .vscode/launch.json | 36 +- .vscode/settings.json | 34 + README.MD | 9 + README.md | 74 - apiFunctions/getHomeData.ts | 38 + apiFunctions/healthCheck.ts | 21 + apiFunctions/postProfile.ts | 77 + apiFunctions/postProject.ts | 82 + constants/actaKeys.ts | 3 + constants/apiRoutes.ts | 49 + constants/appColors.ts | 36 + constants/appConstants.ts | 1 + constants/clientRoutes.ts | 51 + constants/httpCodes.ts | 29 + constants/mongoCollections.ts | 4 + customTypes/project.ts | 27 + customTypes/user.ts | 25 + customTypes/utils/HTTPVerbs.ts | 7 + customTypes/utils/serverError.ts | 8 + dotenvConfig.ts | 3 + next-env.d.ts | 2 + next.config.js | 66 + package.json | 87 +- pages/404.tsx | 19 + pages/_app.tsx | 27 + pages/api/[...route].ts | 116 + pages/index.tsx | 20 + pages/post-profile.tsx | 20 + pages/post-project.tsx | 20 + public/favicon.ico | Bin 22382 -> 0 bytes public/imgs/favicon.png | Bin 0 -> 1772 bytes public/index.html | 43 - public/logo192.png | Bin 8581 -> 0 bytes public/logo512.png | Bin 22920 -> 0 bytes public/manifest.json | 25 - public/robot.txt | 7 + public/robots.txt | 2 - public/sitemap.xml | 11 + src/App.js | 33 - src/Home.js | 15 - src/auth/LoginWrapper.js | 66 - src/auth/LogoutPage.js | 18 - src/data/database.js | 30 - src/features/DemoDataAccess.js | 79 - src/index.js | 4 - src/ui/Card.js | 54 - src/ui/Frame.js | 20 - src/ui/Header.js | 25 - src/ui/Logo.js | 10 - stylelint.config.js | 14 + tsconfig.json | 44 + .../ApplicationHead/ApplicationHead.tsx | 70 + uiComponents/Button/Button.pcss | 124 + uiComponents/Button/Button.tsx | 195 + uiComponents/Button/index.ts | 1 + .../FullHeightContainer.tsx | 30 + uiComponents/FullHeightContainer/index.ts | 1 + uiComponents/GlobalStyles/Animations.pcss | 62 + uiComponents/GlobalStyles/Constants.pcss | 123 + uiComponents/GlobalStyles/GlobalStyles.pcss | 120 + uiComponents/GlobalStyles/GlobalStyles.tsx | 10 + uiComponents/GlobalStyles/index.ts | 1 + uiComponents/Icon/Icon.tsx | 40 + uiComponents/Icon/index.ts | 1 + uiComponents/Input/Input.pcss | 78 + uiComponents/Input/Input.tsx | 212 + uiComponents/Input/index.ts | 1 + uiComponents/InputGroup/InputGroup.pcss | 28 + uiComponents/InputGroup/InputGroup.tsx | 25 + uiComponents/InputGroup/index.ts | 1 + uiComponents/Layout/Layout.pcss | 3 + uiComponents/Layout/Layout.tsx | 28 + uiComponents/Layout/components/AppLayout.pcss | 7 + uiComponents/Layout/components/AppLayout.tsx | 45 + .../Layout/components/PublicLayout.pcss | 3 + .../Layout/components/PublicLayout.tsx | 20 + uiComponents/Layout/index.ts | 1 + uiComponents/Loader/Loader.pcss | 13 + uiComponents/Loader/Loader.tsx | 52 + uiComponents/Loader/index.ts | 1 + uiComponents/PageLoader/PageLoader.pcss | 9 + uiComponents/PageLoader/PageLoader.tsx | 24 + uiComponents/PageLoader/index.ts | 1 + utils/apiRouteGenerator.ts | 205 + utils/dataLayer.ts | 117 + utils/getMongoDB.ts | 47 + .../ensureKeyIs32Bytes.test.ts | 62 + utils/tokenEncryption/ensureKeyIs32Bytes.ts | 21 + .../tokenEncryption/tokenDecrypt.test copy.ts | 30 + utils/tokenEncryption/tokenDecrypt.test.ts | 30 + utils/tokenEncryption/tokenDecrypt.ts | 59 + utils/tokenEncryption/tokenEncrypt.test.ts | 16 + utils/tokenEncryption/tokenEncrypt.ts | 38 + utils/tokenEncryption/userDataPinEncrypt.ts | 59 + utils/toolBelt/base62Id.test.ts | 14 + utils/toolBelt/base62Id.ts | 79 + utils/toolBelt/debounce.ts | 22 + utils/toolBelt/ensureParameters.test.ts | 114 + utils/toolBelt/ensureParameters.ts | 63 + utils/toolBelt/forgeRandomDigitsCode.test.ts | 22 + utils/toolBelt/forgeRandomDigitsCode.ts | 13 + utils/toolBelt/getMobileOperatingSystem.ts | 32 + utils/toolBelt/getUrlParams.test.ts | 25 + utils/toolBelt/getUrlParams.ts | 67 + utils/toolBelt/isObject.test.ts | 53 + utils/toolBelt/isObject.ts | 27 + utils/toolBelt/validateEmail.test.ts | 31 + utils/toolBelt/validateEmail.ts | 9 + views/Home/Home.pcss | 87 + views/Home/Home.tsx | 160 + views/Home/index.ts | 1 + views/Post/Post.pcss | 27 + views/Post/PostProfile.tsx | 154 + views/Post/PostProject.tsx | 165 + views/Post/index.ts | 2 + yarn.lock | 11753 +++++++--------- 126 files changed, 9759 insertions(+), 6840 deletions(-) create mode 100644 modulesDeclarationPCSS.d.ts create mode 100644 modulesDeclarationSVG.d.ts create mode 100644 .babelrc create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .stylelintignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 README.MD delete mode 100644 README.md create mode 100644 apiFunctions/getHomeData.ts create mode 100644 apiFunctions/healthCheck.ts create mode 100644 apiFunctions/postProfile.ts create mode 100644 apiFunctions/postProject.ts create mode 100644 constants/actaKeys.ts create mode 100644 constants/apiRoutes.ts create mode 100644 constants/appColors.ts create mode 100644 constants/appConstants.ts create mode 100644 constants/clientRoutes.ts create mode 100644 constants/httpCodes.ts create mode 100644 constants/mongoCollections.ts create mode 100644 customTypes/project.ts create mode 100644 customTypes/user.ts create mode 100644 customTypes/utils/HTTPVerbs.ts create mode 100644 customTypes/utils/serverError.ts create mode 100644 dotenvConfig.ts create mode 100644 next-env.d.ts create mode 100755 next.config.js create mode 100644 pages/404.tsx create mode 100644 pages/_app.tsx create mode 100644 pages/api/[...route].ts create mode 100644 pages/index.tsx create mode 100644 pages/post-profile.tsx create mode 100644 pages/post-project.tsx delete mode 100644 public/favicon.ico create mode 100644 public/imgs/favicon.png delete mode 100644 public/index.html delete mode 100644 public/logo192.png delete mode 100644 public/logo512.png delete mode 100644 public/manifest.json create mode 100644 public/robot.txt delete mode 100644 public/robots.txt create mode 100644 public/sitemap.xml delete mode 100644 src/App.js delete mode 100644 src/Home.js delete mode 100644 src/auth/LoginWrapper.js delete mode 100644 src/auth/LogoutPage.js delete mode 100644 src/data/database.js delete mode 100644 src/features/DemoDataAccess.js delete mode 100644 src/index.js delete mode 100644 src/ui/Card.js delete mode 100644 src/ui/Frame.js delete mode 100644 src/ui/Header.js delete mode 100644 src/ui/Logo.js create mode 100644 stylelint.config.js create mode 100644 tsconfig.json create mode 100644 uiComponents/ApplicationHead/ApplicationHead.tsx create mode 100644 uiComponents/Button/Button.pcss create mode 100644 uiComponents/Button/Button.tsx create mode 100644 uiComponents/Button/index.ts create mode 100644 uiComponents/FullHeightContainer/FullHeightContainer.tsx create mode 100644 uiComponents/FullHeightContainer/index.ts create mode 100644 uiComponents/GlobalStyles/Animations.pcss create mode 100644 uiComponents/GlobalStyles/Constants.pcss create mode 100644 uiComponents/GlobalStyles/GlobalStyles.pcss create mode 100644 uiComponents/GlobalStyles/GlobalStyles.tsx create mode 100644 uiComponents/GlobalStyles/index.ts create mode 100644 uiComponents/Icon/Icon.tsx create mode 100644 uiComponents/Icon/index.ts create mode 100644 uiComponents/Input/Input.pcss create mode 100644 uiComponents/Input/Input.tsx create mode 100644 uiComponents/Input/index.ts create mode 100644 uiComponents/InputGroup/InputGroup.pcss create mode 100644 uiComponents/InputGroup/InputGroup.tsx create mode 100644 uiComponents/InputGroup/index.ts create mode 100644 uiComponents/Layout/Layout.pcss create mode 100644 uiComponents/Layout/Layout.tsx create mode 100644 uiComponents/Layout/components/AppLayout.pcss create mode 100644 uiComponents/Layout/components/AppLayout.tsx create mode 100644 uiComponents/Layout/components/PublicLayout.pcss create mode 100644 uiComponents/Layout/components/PublicLayout.tsx create mode 100644 uiComponents/Layout/index.ts create mode 100644 uiComponents/Loader/Loader.pcss create mode 100644 uiComponents/Loader/Loader.tsx create mode 100644 uiComponents/Loader/index.ts create mode 100644 uiComponents/PageLoader/PageLoader.pcss create mode 100644 uiComponents/PageLoader/PageLoader.tsx create mode 100644 uiComponents/PageLoader/index.ts create mode 100644 utils/apiRouteGenerator.ts create mode 100644 utils/dataLayer.ts create mode 100644 utils/getMongoDB.ts create mode 100644 utils/tokenEncryption/ensureKeyIs32Bytes.test.ts create mode 100644 utils/tokenEncryption/ensureKeyIs32Bytes.ts create mode 100644 utils/tokenEncryption/tokenDecrypt.test copy.ts create mode 100644 utils/tokenEncryption/tokenDecrypt.test.ts create mode 100644 utils/tokenEncryption/tokenDecrypt.ts create mode 100644 utils/tokenEncryption/tokenEncrypt.test.ts create mode 100644 utils/tokenEncryption/tokenEncrypt.ts create mode 100644 utils/tokenEncryption/userDataPinEncrypt.ts create mode 100644 utils/toolBelt/base62Id.test.ts create mode 100644 utils/toolBelt/base62Id.ts create mode 100644 utils/toolBelt/debounce.ts create mode 100644 utils/toolBelt/ensureParameters.test.ts create mode 100644 utils/toolBelt/ensureParameters.ts create mode 100644 utils/toolBelt/forgeRandomDigitsCode.test.ts create mode 100644 utils/toolBelt/forgeRandomDigitsCode.ts create mode 100644 utils/toolBelt/getMobileOperatingSystem.ts create mode 100644 utils/toolBelt/getUrlParams.test.ts create mode 100644 utils/toolBelt/getUrlParams.ts create mode 100644 utils/toolBelt/isObject.test.ts create mode 100644 utils/toolBelt/isObject.ts create mode 100644 utils/toolBelt/validateEmail.test.ts create mode 100644 utils/toolBelt/validateEmail.ts create mode 100644 views/Home/Home.pcss create mode 100644 views/Home/Home.tsx create mode 100644 views/Home/index.ts create mode 100644 views/Post/Post.pcss create mode 100644 views/Post/PostProfile.tsx create mode 100644 views/Post/PostProject.tsx create mode 100644 views/Post/index.ts diff --git a/ modulesDeclarationPCSS.d.ts b/ modulesDeclarationPCSS.d.ts new file mode 100644 index 0000000..8e5f75f --- /dev/null +++ b/ modulesDeclarationPCSS.d.ts @@ -0,0 +1,7 @@ +declare module '*.pcss' { + const content: { + hash: string; + styles: string; + }; + export default content; +} diff --git a/ modulesDeclarationSVG.d.ts b/ modulesDeclarationSVG.d.ts new file mode 100644 index 0000000..091d25e --- /dev/null +++ b/ modulesDeclarationSVG.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: any; + export default content; +} diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..1ff94f7 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["next/babel"] +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..35e915e --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +**/node_modules/* +**/out/* +**/.next/* diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..a6de3a5 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,100 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "settings": { + "react": { + "createClass": "createReactClass", + "pragma": "React", + "version": "detect" + } + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended" + // Uncomment the following lines to enable eslint-config-prettier + // Is not enabled right now to avoid issues with the Next.js repo + // "prettier", + // "prettier/@typescript-eslint" + ], + "env": { + "es6": true, + "browser": true, + "jest": true, + "node": true + }, + "rules": { + "no-empty-pattern": 0, + "react/react-in-jsx-scope": 0, + "react/display-name": 0, + "react/prop-types": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/explicit-member-accessibility": 0, + "@typescript-eslint/indent": 0, + "@typescript-eslint/member-delimiter-style": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-use-before-define": 0, + "@typescript-eslint/ban-ts-comment": [ + 2, + { + "ts-expect-error": "allow-with-description", + "ts-ignore": "allow-with-description", + "ts-nocheck": "allow-with-description", + "ts-check": "allow-with-description", + "minimumDescriptionLength": 3 + } + ], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "default", + "format": ["PascalCase", "camelCase"], + "trailingUnderscore": "forbid" + }, + { + "selector": "interface", + "prefix": ["I"], + "format": ["PascalCase"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + }, + { + "selector": "enum", + "prefix": ["E"], + "format": ["PascalCase"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + }, + { + "selector": "enumMember", + "format": ["UPPER_CASE"], + "leadingUnderscore": "forbid", + "trailingUnderscore": "forbid" + }, + { + "selector": "property", + "format": ["camelCase"], + "trailingUnderscore": "forbid" + }, + { + "selector": "parameter", + "format": ["camelCase"], + "leadingUnderscore": "allow", + "trailingUnderscore": "forbid" + } + ], + "@typescript-eslint/no-unused-vars": [ + 2, + { + "argsIgnorePattern": "^_" + } + ], + "no-console": [ + 2, + { + "allow": ["warn", "error"] + } + ] + } +} diff --git a/.gitignore b/.gitignore index 4d29575..bf6ec36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,31 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -# dependencies -/node_modules -/.pnp -.pnp.js +# node modules should never be commit +**/node_modules/ -# testing -/coverage +# local vars & env vars +**/.env +**/.env.local +**/.vercel +**/.next +**/.env*.local -# production -/build +# testing +coverage # misc .DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local +# mongod data +data + +# build target directories should stay on your machine +**/bin +**/dist + +# debug dumps should stay on your machine npm-debug.log* yarn-debug.log* yarn-error.log* + + diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..4c1112d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +node_modules +.next +yarn.lock +package-lock.json +public diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c7da016 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "bracketSpacing": true, + "jsxBracketSameLine": true, + "printWidth": 80, + "proseWrap": "preserve", + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": false +} diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000..6fd10a4 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1 @@ +/node_modules/* \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..bf650c7 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "editorconfig.editorconfig" + ], + "unwantedRecommendations": ["hookyqr.beautify", "dbaeumer.jshint"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 2ba986f..dbac113 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,15 +1,23 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Launch Chrome against localhost", - "url": "http://localhost:8080", - "webRoot": "${workspaceFolder}" - } - ] -} \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jest Test Current eslint-plugin Rule", + "cwd": "${workspaceFolder}/packages/eslint-plugin/", + "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", + "args": [ + "--runInBand", + // needs the '' around it so that the () are properly handled + "'tests/(.+/)?${fileBasenameNoExtension}'" + ], + "sourceMaps": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..99e618c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,34 @@ +{ + // An array of language ids which should be validated by ESLint + "eslint.validate": [ + "javascript", + "javascriptreact", + { + "language": "typescriptreact", + "autoFix": true + } + ], + "files.associations": { + "*.ts": "typescriptreact" + }, + "editor.tabSize": 2, + "editor.formatOnSave": true, + "git.confirmSync": false, + "git.autofetch": true, + "prettier.singleQuote": true, + "prettier.trailingComma": "all", + "prettier.printWidth": 80, + "prettier.useTabs": false, + "prettier.tabWidth": 2, + "prettier.jsxBracketSameLine": true, + "prettier.bracketSpacing": true, + "prettier.semi": true, + "prettier.proseWrap": "preserve", + + // When enabled, will trim trailing whitespace when saving a file. + "files.trimTrailingWhitespace": true, + + // typescript auto-format settings + "javascript.preferences.importModuleSpecifier": "auto", + "typescript.preferences.quoteStyle": "single" +} diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..c06d546 --- /dev/null +++ b/README.MD @@ -0,0 +1,9 @@ +# OKfounder + +Let's found!! + +run in the folder: + +- `curl https://knox-app.com/api/get-secrets?format=envfile&token=e57540dc241056a5d47b34eef1c652d8:7dab73308ea12ce291baf0a81d69cbf4f0646253c59defeb58db1df6b63428004683abbf84d570d0ff4fb568b813979b7fd1708983d42fd54c35e9578600fd4d > .env` +- `yarn` +- `yarn run dev` diff --git a/README.md b/README.md deleted file mode 100644 index 18419e6..0000000 --- a/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# The Project - -## "OkFounder" - -You are the founder of a startup called OkFounder. Your startup's mission is to help entrepreneurs find a co-founder. You've found a Venture Capitalist who thinks this is a good idea, and you're meeting her in a few hours. The only problem is: you don't have a product! - -## The task - -We'd like you to build an MVP version of your "co-founder dating" product. It should be a web app that lets entrepreneurs connect with other entrepreneurs who might become their co-founder. - -The priority is to build a [true MVP](https://public-media.interaction-design.org/images/uploads/9f7f5b30ed9905117b65572ab6949a9f.png) – something that solves the smallest possible version of the problem. - -We'd like you to spend no more than 3 hours on it*. You should feel free to cut the scope as liberally as you like, and use any shortcuts (product or tech) that you like. - -Also, to help save time on the things that are slow: - -- Don't build any secure authentication – we'll let the users log in with just their email address -- Don't worry about building a backend – this starter kit includes a localStorage hosted database -- Don't worry about security – this is all all going to be demoed on your laptop -- Don't worry about code quality or testing – if you've got this far we're already confident that you can write good code! - -The priority here is to build something which **works**, and **solves the user need.** - - *We mean that. This isn't code for "spend 18 hours on it and tell us it was 3 to impress". We want to embrace the limitations of creating something in a short time. - -### What we're interested in - -The point of this task is not to see how many features you can build. - -We want to see: - -- how you go about solving the product problem -- how you embrace limitations and make pragmatic product and tech choices so that you have *something* that's demoable by the end. - - -### The codebase - -Provided here is a very simple starter kit for the app. - -It includes: - -- Basic app structure (header, etc) -- Logging in (via username, no password necessary!) and logging out -- A [UI component library](https://chakra-ui.com/) -- A sample localStorage database and demo component for how to read and write to that db - -![image](https://user-images.githubusercontent.com/965059/69821009-5f80e380-11fa-11ea-99a1-29b5bc5405fb.png) - - - -### Getting started - -1. Fork this repo -2. Clone it to your machine -3. Run `npm install` -4. Run `npm start` -5. Get creating! - - -### The output - -- You should have forked this repo and make your changes there. Once you're done, make a PR back into this repo (see [here](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork)) -- We'd like to be able to run this ourselves, so please don't add any dependencies to services on your machine. Adding new node modules is fine though! - - ----- - -# Meta: Why we ask you to do a project - -We have two aims when asking you to do this project. - -Firstly, we'd like to see how you approach a product problem on your own. We don't think "test conditions" gives a good indication of how someone performs when they're at their best, so this gives us a way to see what you do when working on your terms. - -Secondly, we want something that we can pair with you on. We've found that's best when it's a 'greenfield' project in a codebase you're familiar with. diff --git a/apiFunctions/getHomeData.ts b/apiFunctions/getHomeData.ts new file mode 100644 index 0000000..f1211f8 --- /dev/null +++ b/apiFunctions/getHomeData.ts @@ -0,0 +1,38 @@ +/** + * Due to stupid vercel limitation, we will + * limit the number of api endpoints. So this + * endpoint will manage all the datacalls that + * users will do when they arrive on a view + */ +import { NowRequest, NowResponse } from '@now/node'; + +import { apiRouteGenerator } from '@utils/apiRouteGenerator'; +import { EHTTPSuccessCode } from '@constants/httpCodes'; +import { apiRoutes, EAPIRouteKeys } from '@constants/apiRoutes'; +import { getMongoDB } from '@utils/getMongoDB'; +import { EMongoCollections } from '@constants/mongoCollections'; + +export const getHomeData = apiRouteGenerator({ + method: apiRoutes[EAPIRouteKeys.GET_HOME_DATA].method, + routeFunction: async ( + _clientRequest: NowRequest, + serverResponse: NowResponse, + ) => { + /** + * Collection + */ + const db = await getMongoDB(); + const usersCollection = db.collection(EMongoCollections.USERS); + const projectsCollection = db.collection(EMongoCollections.PROJECTS); + + const [users, projects] = await Promise.all([ + await usersCollection.find().toArray(), + await projectsCollection.find().toArray(), + ]); + + return serverResponse.status(EHTTPSuccessCode.CODE_200).send({ + users, + projects, + }); + }, +}); diff --git a/apiFunctions/healthCheck.ts b/apiFunctions/healthCheck.ts new file mode 100644 index 0000000..fadf9a9 --- /dev/null +++ b/apiFunctions/healthCheck.ts @@ -0,0 +1,21 @@ +/** + * Due to stupid vercel limitation, we will + * limit the number of api endpoints. So this + * endpoint will manage all the datacalls that + * users will do when they arrive on a view + */ +import { NowRequest, NowResponse } from '@now/node'; + +import { apiRouteGenerator } from '@utils/apiRouteGenerator'; +import { EHTTPSuccessCode } from '@constants/httpCodes'; +import { apiRoutes, EAPIRouteKeys } from '@constants/apiRoutes'; + +export const healthCheck = apiRouteGenerator({ + method: apiRoutes[EAPIRouteKeys.HEALTH_CHECK].method, + routeFunction: async ( + _clientRequest: NowRequest, + serverResponse: NowResponse, + ) => { + return serverResponse.status(EHTTPSuccessCode.CODE_200).send('OK'); + }, +}); diff --git a/apiFunctions/postProfile.ts b/apiFunctions/postProfile.ts new file mode 100644 index 0000000..c541ab3 --- /dev/null +++ b/apiFunctions/postProfile.ts @@ -0,0 +1,77 @@ +/** + * Due to stupid vercel limitation, we will + * limit the number of api endpoints. So this + * endpoint will manage all the datacalls that + * users will do when they arrive on a view + */ +import { NowRequest, NowResponse } from '@now/node'; + +import { apiRouteGenerator } from '@utils/apiRouteGenerator'; +import { EHTTPSuccessCode } from '@constants/httpCodes'; +import { apiRoutes, EAPIRouteKeys } from '@constants/apiRoutes'; +import { getMongoDB } from '@utils/getMongoDB'; +import { EMongoCollections } from '@constants/mongoCollections'; +import { EUserKeys } from '@customTypes/user'; + +export const postProfile = apiRouteGenerator({ + method: apiRoutes[EAPIRouteKeys.POST_PROFILE].method, + requiredBodyKeys: [ + { + key: EUserKeys.DONTWANT, + type: 'string', + }, + { + key: EUserKeys.EMAIL, + type: 'string', + }, + { + key: EUserKeys.FIRSTNAME, + type: 'string', + }, + { + key: EUserKeys.LASTNAME, + type: 'string', + }, + { + key: EUserKeys.LINKEDIN, + type: 'string', + }, + { + key: EUserKeys.MOTIVATION, + type: 'string', + }, + { + key: EUserKeys.SKILLSET, + type: 'string', + }, + { + key: EUserKeys.WANT, + type: 'string', + }, + ], + routeFunction: async ( + clientRequest: NowRequest, + serverResponse: NowResponse, + ) => { + /** + * Collection + */ + const db = await getMongoDB(); + const usersCollection = db.collection(EMongoCollections.USERS); + + const response = await usersCollection.insertOne({ + [EUserKeys.DONTWANT]: clientRequest.body[EUserKeys.DONTWANT], + [EUserKeys.EMAIL]: clientRequest.body[EUserKeys.EMAIL], + [EUserKeys.FIRSTNAME]: clientRequest.body[EUserKeys.FIRSTNAME], + [EUserKeys.LASTNAME]: clientRequest.body[EUserKeys.LASTNAME], + [EUserKeys.LINKEDIN]: clientRequest.body[EUserKeys.LINKEDIN], + [EUserKeys.MOTIVATION]: clientRequest.body[EUserKeys.MOTIVATION], + [EUserKeys.SKILLSET]: clientRequest.body[EUserKeys.SKILLSET], + [EUserKeys.WANT]: clientRequest.body[EUserKeys.WANT], + }); + + return serverResponse + .status(EHTTPSuccessCode.CODE_200) + .send(response.ops[0]); + }, +}); diff --git a/apiFunctions/postProject.ts b/apiFunctions/postProject.ts new file mode 100644 index 0000000..055a9ec --- /dev/null +++ b/apiFunctions/postProject.ts @@ -0,0 +1,82 @@ +/** + * Due to stupid vercel limitation, we will + * limit the number of api endpoints. So this + * endpoint will manage all the datacalls that + * users will do when they arrive on a view + */ +import { NowRequest, NowResponse } from '@now/node'; + +import { apiRouteGenerator } from '@utils/apiRouteGenerator'; +import { EHTTPSuccessCode } from '@constants/httpCodes'; +import { apiRoutes, EAPIRouteKeys } from '@constants/apiRoutes'; +import { getMongoDB } from '@utils/getMongoDB'; +import { EMongoCollections } from '@constants/mongoCollections'; +import { EProjectKeys } from '@customTypes/project'; + +export const postProject = apiRouteGenerator({ + method: apiRoutes[EAPIRouteKeys.POST_PROFILE].method, + requiredBodyKeys: [ + { + key: EProjectKeys.DESCRIPTION, + type: 'string', + }, + { + key: EProjectKeys.EMAIL, + type: 'string', + }, + { + key: EProjectKeys.FIRSTNAME, + type: 'string', + }, + { + key: EProjectKeys.LASTNAME, + type: 'string', + }, + { + key: EProjectKeys.LINKEDIN, + type: 'string', + }, + { + key: EProjectKeys.NAME, + type: 'string', + }, + { + key: EProjectKeys.SKILLSET, + type: 'string', + }, + { + key: EProjectKeys.TARGET, + type: 'string', + }, + { + key: EProjectKeys.TYPE, + type: 'string', + }, + ], + routeFunction: async ( + clientRequest: NowRequest, + serverResponse: NowResponse, + ) => { + /** + * Collection + */ + const db = await getMongoDB(); + const projectsCollection = db.collection(EMongoCollections.PROJECTS); + + const response = await projectsCollection.insertOne({ + [EProjectKeys.DESCRIPTION]: clientRequest.body[EProjectKeys.DESCRIPTION], + [EProjectKeys.EMAIL]: clientRequest.body[EProjectKeys.EMAIL], + [EProjectKeys.FIRSTNAME]: clientRequest.body[EProjectKeys.FIRSTNAME], + [EProjectKeys.LASTNAME]: clientRequest.body[EProjectKeys.LASTNAME], + [EProjectKeys.LINKEDIN]: clientRequest.body[EProjectKeys.LINKEDIN], + [EProjectKeys.NAME]: clientRequest.body[EProjectKeys.NAME], + [EProjectKeys.SKILLSET]: clientRequest.body[EProjectKeys.SKILLSET], + [EProjectKeys.TARGET]: clientRequest.body[EProjectKeys.TARGET], + [EProjectKeys.TYPE]: clientRequest.body[EProjectKeys.TYPE], + }); + + return serverResponse + .status(EHTTPSuccessCode.CODE_200) + .send(response.ops[0]); + }, +}); diff --git a/constants/actaKeys.ts b/constants/actaKeys.ts new file mode 100644 index 0000000..e257900 --- /dev/null +++ b/constants/actaKeys.ts @@ -0,0 +1,3 @@ +export enum EActaEventKeys { + APPLICATION_MESSAGE = 'APPLICATION_MESSAGE', +} diff --git a/constants/apiRoutes.ts b/constants/apiRoutes.ts new file mode 100644 index 0000000..db8a4aa --- /dev/null +++ b/constants/apiRoutes.ts @@ -0,0 +1,49 @@ +import { EHTTPVerbs } from '@customTypes/utils/HTTPVerbs'; + +export enum EAPIRouteKeys { + /** + * Health check + */ + HEALTH_CHECK = 'HEALTH_CHECK', + + /** + * Data + */ + GET_HOME_DATA = 'GET_HOME_DATA', + + /** + * Post + */ + POST_PROJECT = 'POST_PROJECT', + POST_PROFILE = 'POST_PROFILE', +} + +export const apiRoutes = { + /** + * Health check + */ + [EAPIRouteKeys.HEALTH_CHECK]: { + route: 'health-check', + method: EHTTPVerbs.GET, + }, + + /** + * Data + */ + [EAPIRouteKeys.GET_HOME_DATA]: { + route: 'get-home-data', + method: EHTTPVerbs.GET, + }, + + /** + * Post + */ + [EAPIRouteKeys.POST_PROJECT]: { + route: 'post-project', + method: EHTTPVerbs.POST, + }, + [EAPIRouteKeys.POST_PROFILE]: { + route: 'post-profile', + method: EHTTPVerbs.POST, + }, +}; diff --git a/constants/appColors.ts b/constants/appColors.ts new file mode 100644 index 0000000..fa6795a --- /dev/null +++ b/constants/appColors.ts @@ -0,0 +1,36 @@ +export enum EAppColors { + PRIMARY = '#2a9d8f', + PRIMARY_LIGHT = '#1bccb6', + PRIMARY_DARK = '#264653', + PRIMARY_TEXT = '#1a3066', + PRIMARY_TEXT_LIGHT = '#4a5e92', + SECONDARY_TEXT = '#1a3066', + BG = '#0c0f14', + + WHITE = '#fff', + + ACCENT = '#f4a261', + ACCENT_LIGHT = '#e9c46a', + ACCENT_DARK = '#e76f51', + + SUCCESS = '#69bfa0', + SUCCESS_LIGHT = '#b8e5d3', + SUCCESS_LIGHTER = '#dff2ea', + SUCCESS_DARK = '#008055', + SUCCESS_DARKER = '#006647', + SUCCESS_TEXT = '#191919', + + WARNING = '#e8a126', + WARNING_LIGHT = '#ffdea6', + WARNING_LIGHTER = '#fff1d9', + WARNING_DARK = '#cc8100', + WARNING_DARKER = '#994d00', + WARNING_TEXT = '#191919', + + ERROR = '#e58989', + ERROR_LIGHT = '#f2cece', + ERROR_LIGHTER = '#faebeb', + ERROR_DARK = '#b20000', + ERROR_DARKER = '#990000', + ERROR_TEXT = '#191919', +} diff --git a/constants/appConstants.ts b/constants/appConstants.ts new file mode 100644 index 0000000..7a10a81 --- /dev/null +++ b/constants/appConstants.ts @@ -0,0 +1 @@ +export enum EApplicationConstants {} diff --git a/constants/clientRoutes.ts b/constants/clientRoutes.ts new file mode 100644 index 0000000..7107c4b --- /dev/null +++ b/constants/clientRoutes.ts @@ -0,0 +1,51 @@ +/** + * This file will list all the client route used + * in the public sites and privte application + */ + +/** + * An enum containing all the unique names of the routes + */ +export enum EClientRouteKeys { + /** + * Post routes + */ + POST_PROJECT = 'POST_PROJECT', + POST_PROFILE = 'POST_PROFILE', + + /** + * Static elements + */ + FRONT_HOME = 'FRONT_HOME', +} + +/** + * An object litteral with all the client routes + */ +export const clientRoutes = { + /** + * Static front end routes + */ + [EClientRouteKeys.FRONT_HOME]: { + path: '/[language]-[country]', + pathBuilder: ({ + language, + country, + }: { + language?: string; + country?: string; + }): string => { + if (language && !country) { + return `/${language}`; + } else if (language && country) { + return `/${language}-${country}`; + } else return '/'; + }, + }, + + /** + * Post routes + */ + [EClientRouteKeys.POST_PROJECT]: { path: '/post-project' }, + [EClientRouteKeys.POST_PROFILE]: { path: '/post-profile' }, +}; diff --git a/constants/httpCodes.ts b/constants/httpCodes.ts new file mode 100644 index 0000000..337c81d --- /dev/null +++ b/constants/httpCodes.ts @@ -0,0 +1,29 @@ +/** + * We should not use to much different error codes + * Here are the allowed ones + */ + +export enum EHTTPSuccessCode { + CODE_200 = 200, // OK -- Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request, the response will contain an entity describing or containing the result of the action. + CODE_201 = 201, // Created -- The request has been fulfilled, resulting in the creation of a new resource. +} + +export enum EHTTPRedirectCode { + CODE_301 = 301, // Moved Permanently -- This and all future requests should be directed to the given URI. + CODE_302 = 302, // Found (Previously "Moved temporarily") -- Tells the client to look at (browse to) another URL. 302 has been superseded by 303 and 307. This is an example of industry practice contradicting the standard. The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect (the original describing phrase was "Moved Temporarily"), +} + +export enum EHTTPErrorCode { + CODE_400 = 400, // Bad Request -- The server cannot or will not process the request due to an apparent client error (e.g., malformed request syntax, size too large, invalid request message framing, or deceptive request routing). + CODE_401 = 401, // Unauthorized -- Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. + CODE_403 = 403, // Forbidden -- The request contained valid data and was understood by the server, but the server is refusing action. + CODE_404 = 404, // Not Found -- The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. + CODE_405 = 405, // Method Not Allowed -- A request method is not supported for the requested resource; for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. + CODE_413 = 413, // Request Entity Too Large -- The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large". + CODE_429 = 429, // Too Many Requests -- The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. + + CODE_500 = 500, // Internal Server Error -- A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. + CODE_501 = 501, // Not Implemented -- The server either does not recognize the request method, or it lacks the ability to fulfil the request. Usually this implies future availability (e.g., a new feature of a web-service API). + CODE_503 = 503, // Service Unavailable -- The server cannot handle the request (because it is overloaded or down for maintenance). Generally, this is a temporary state. + CODE_504 = 504, // Gateway Time-out -- The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. +} diff --git a/constants/mongoCollections.ts b/constants/mongoCollections.ts new file mode 100644 index 0000000..26804bd --- /dev/null +++ b/constants/mongoCollections.ts @@ -0,0 +1,4 @@ +export enum EMongoCollections { + USERS = 'users', + PROJECTS = 'projects', +} diff --git a/customTypes/project.ts b/customTypes/project.ts new file mode 100644 index 0000000..be7e0f5 --- /dev/null +++ b/customTypes/project.ts @@ -0,0 +1,27 @@ +import { ObjectId } from 'mongodb'; + +export enum EProjectKeys { + DESCRIPTION = 'description', + EMAIL = 'email', + NAME = 'name', + FIRSTNAME = 'firstName', + ID = '_id', + LASTNAME = 'lastName', + LINKEDIN = 'linkedin', + SKILLSET = 'skillset', + TARGET = 'target', + TYPE = 'type', +} + +export interface IProject { + [EProjectKeys.ID]?: ObjectId; + [EProjectKeys.DESCRIPTION]: string; + [EProjectKeys.EMAIL]: string; + [EProjectKeys.NAME]: string; + [EProjectKeys.FIRSTNAME]: string; + [EProjectKeys.LASTNAME]: string; + [EProjectKeys.LINKEDIN]: string; + [EProjectKeys.SKILLSET]: string; + [EProjectKeys.TARGET]: string; + [EProjectKeys.TYPE]: string; +} diff --git a/customTypes/user.ts b/customTypes/user.ts new file mode 100644 index 0000000..145f220 --- /dev/null +++ b/customTypes/user.ts @@ -0,0 +1,25 @@ +import { ObjectId } from 'mongodb'; + +export enum EUserKeys { + DONTWANT = 'dontwant', + EMAIL = 'email', + FIRSTNAME = 'firstName', + ID = '_id', + LASTNAME = 'lastName', + LINKEDIN = 'linkedin', + MOTIVATION = 'motivation', + SKILLSET = 'skillset', + WANT = 'want', +} + +export interface IUser { + [EUserKeys.ID]?: ObjectId; + [EUserKeys.DONTWANT]: string; + [EUserKeys.EMAIL]: string; + [EUserKeys.FIRSTNAME]: string; + [EUserKeys.LASTNAME]: string; + [EUserKeys.LINKEDIN]: string; + [EUserKeys.MOTIVATION]: string; + [EUserKeys.SKILLSET]: string; + [EUserKeys.WANT]: string; +} diff --git a/customTypes/utils/HTTPVerbs.ts b/customTypes/utils/HTTPVerbs.ts new file mode 100644 index 0000000..8a4d9ea --- /dev/null +++ b/customTypes/utils/HTTPVerbs.ts @@ -0,0 +1,7 @@ +export enum EHTTPVerbs { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE', +} diff --git a/customTypes/utils/serverError.ts b/customTypes/utils/serverError.ts new file mode 100644 index 0000000..20647ae --- /dev/null +++ b/customTypes/utils/serverError.ts @@ -0,0 +1,8 @@ +import { EHTTPErrorCode } from '@constants/httpCodes'; + +export interface IServerError { + error: { + code: EHTTPErrorCode; + message: string; + }; +} diff --git a/dotenvConfig.ts b/dotenvConfig.ts new file mode 100644 index 0000000..d274743 --- /dev/null +++ b/dotenvConfig.ts @@ -0,0 +1,3 @@ +import dotenv from 'dotenv'; + +dotenv.config(); diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..7b7aa2c --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/next.config.js b/next.config.js new file mode 100755 index 0000000..1352413 --- /dev/null +++ b/next.config.js @@ -0,0 +1,66 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const path = require('path'); +require('dotenv').config(); + +module.exports = { + async headers() { + return [ + { + // mathching all API routes + source: '/api/:path*', + headers: [ + { key: 'Access-Control-Allow-Credentials', value: 'true' }, + { key: 'Access-Control-Allow-Origin', value: '*' }, + { + key: 'Access-Control-Allow-Methods', + value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT', + }, + { + key: 'Access-Control-Allow-Headers', + value: + 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version', + }, + ], + }, + ]; + }, + webpack(config) { + config.resolve.alias = { + ...(config.resolve.alias || {}), + '@apiFunctions': path.resolve(__dirname, 'apiFunctions'), + '@constants': path.resolve(__dirname, 'constants'), + '@pages': path.resolve(__dirname, 'pages'), + '@redirects': path.resolve(__dirname, 'redirects'), + '@customTypes': path.resolve(__dirname, 'customTypes'), + '@uiAssets': path.resolve(__dirname, 'uiAssets'), + '@uiComponents': path.resolve(__dirname, 'uiComponents'), + '@utils': path.resolve(__dirname, 'utils'), + '@views': path.resolve(__dirname, 'views'), + '@root': path.resolve(__dirname, ''), + }; + + config.module.rules.push({ + test: /\.pcss$/i, + exclude: /node_modules/, + use: { + loader: 'pcss-loader', + options: { + minified: true, + }, + }, + }); + + config.module.rules.push({ + test: /\.svg$/, + issuer: { + test: /\.(js|ts)x?$/, + }, + use: ['@svgr/webpack'], + }); + + return config; + }, + poweredByHeader: false, + target: 'serverless', + trailingSlash: false, +}; diff --git a/package.json b/package.json index 7f6d4a8..36a9b97 100644 --- a/package.json +++ b/package.json @@ -1,37 +1,64 @@ { - "name": "cofounder-dating-project", - "version": "0.1.0", - "private": true, - "dependencies": { - "@chakra-ui/core": "^0.4.1", - "@emotion/core": "^10.0.22", - "@emotion/styled": "^10.0.23", - "emotion-theming": "^10.0.19", - "localstoragedb": "^2.3.2", - "react": "^16.12.0", - "react-dom": "^16.12.0", - "react-router-dom": "^5.1.2", - "react-scripts": "3.2.0" - }, + "name": "okfounder", + "author": "Fabien Huet", + "version": "1.0.0", + "license": "UNLICENSED", "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "dev_database": "mkdir -p data && mongod --dbpath=data --port 3001", + "dev": "next dev", + "build": "next build", + "start": "next start", + "type-check": "tsc --pretty --noEmit", + "format": "prettier --write **/*.{js,ts,tsx}", + "lint": "eslint . --ext ts --ext tsx --ext js" }, - "eslintConfig": { - "extends": "react-app" + "husky": { + "hooks": { + "pre-commit": "lint-staged", + "pre-push": "yarn run type-check" + } }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" + "lint-staged": { + "*.@(ts|tsx)": [ + "yarn lint", + "yarn format" ] + }, + "dependencies": { + "@mdi/js": "^5.8.55", + "@now/node": "^1.8.1", + "acta": "^3.0.13", + "dotenv": "^8.2.0", + "mongodb": "^3.6.3", + "next": "^10.0.3", + "pcss-loader": "^1.0.0", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "react-styles-injector": "^1.1.5", + "sass": "^1.30.0" + }, + "devDependencies": { + "@babel/core": "^7.12.9", + "@babel/preset-env": "^7.12.7", + "@svgr/webpack": "^5.5.0", + "@testing-library/react": "^11.2.2", + "@types/jest": "^26.0.18", + "@types/mongodb": "^3.6.3", + "@types/node": "^14.14.11", + "@types/react": "^17.0.0", + "@types/testing-library__react": "^10.2.0", + "@typescript-eslint/eslint-plugin": "^4.9.1", + "@typescript-eslint/parser": "^4.9.1", + "babel-loader": "^8.2.2", + "eslint": "^7.15.0", + "eslint-config-prettier": "^7.0.0", + "eslint-plugin-react": "^7.21.5", + "husky": "^4.3.5", + "lint-staged": "^10.5.3", + "prettier": "^2.2.1", + "stylelint": "^13.8.0", + "stylelint-config-standard": "^20.0.0", + "typescript": "^4.1.2", + "webpack": "^5.10.0" } } diff --git a/pages/404.tsx b/pages/404.tsx new file mode 100644 index 0000000..35fbf94 --- /dev/null +++ b/pages/404.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { Layout } from '@uiComponents/Layout'; +import { ERouteType } from '@uiComponents/Layout/Layout'; +import { NextPage } from 'next'; +import { ApplicationHead } from '@uiComponents/ApplicationHead/ApplicationHead'; + +export class NotFoundPage extends React.Component { + public render(): JSX.Element { + return ( + + + 404 not found + + ); + } +} + +export default NotFoundPage; diff --git a/pages/_app.tsx b/pages/_app.tsx new file mode 100644 index 0000000..4160120 --- /dev/null +++ b/pages/_app.tsx @@ -0,0 +1,27 @@ +import { AppProps } from 'next/app'; + +import { GlobalStyles } from '@uiComponents/GlobalStyles'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const App = ({ Component, pageProps }: AppProps): JSX.Element => { + /** + * If there is a sponsor code in the query params, set it up + * in the local storage for future use + */ + if (typeof window !== 'undefined') { + /** + * When navigating to a new page, scroll to the + * top of the said page + */ + document.body.scrollTo(0, 0); + } + + return ( + <> + + + + ); +}; + +export default App; diff --git a/pages/api/[...route].ts b/pages/api/[...route].ts new file mode 100644 index 0000000..88f2cbe --- /dev/null +++ b/pages/api/[...route].ts @@ -0,0 +1,116 @@ +import { NowRequest, NowResponse } from '@now/node'; +import { EAPIRouteKeys, apiRoutes } from '@constants/apiRoutes'; +import { EHTTPErrorCode } from '@constants/httpCodes'; +import { IServerError } from '@customTypes/utils/serverError'; + +import { healthCheck } from '@apiFunctions/healthCheck'; +import { getHomeData } from '@apiFunctions/getHomeData'; +import { postProfile } from '@apiFunctions/postProfile'; +import { postProject } from '@apiFunctions/postProject'; + +/** + * One endpoint to manage the entire api and the actual route + * as an URL param may seems weird. But this architecture + * exists fro two reasons : + * - the limitations of the Vercel platform that allows + * a maximum of 21 functions per deployment (this is + * low) and a maximum of 640 edits per month to the + * functions (this is super low) + * - everything being a function, it will be super easy + * to refactor to something else, should we decide to + * change platform + */ +export const handler = async ( + clientRequest: NowRequest, + serverResponse: NowResponse, +): Promise => { + let actualRoute = clientRequest.query.route; + if (Array.isArray(actualRoute)) { + actualRoute = actualRoute.join('/'); + } + + /** + * In dev mode, log all the requests and simulate + * a 500ms delay on all requests to see the loading time + */ + if (process.env.NODE_ENV === 'development') { + // eslint-disable-next-line no-console + console.info({ + url: clientRequest.url, + apiRoute: actualRoute, + headers: clientRequest.headers || {}, + query: clientRequest.query || {}, + body: clientRequest.body || {}, + }); + } + + try { + switch (actualRoute) { + /** + * Health check + */ + case apiRoutes[EAPIRouteKeys.HEALTH_CHECK].route: + return healthCheck(clientRequest, serverResponse); + + /** + * Data + */ + case apiRoutes[EAPIRouteKeys.GET_HOME_DATA].route: + return getHomeData(clientRequest, serverResponse); + + /** + * Post + */ + case apiRoutes[EAPIRouteKeys.POST_PROFILE].route: + return postProfile(clientRequest, serverResponse); + case apiRoutes[EAPIRouteKeys.POST_PROJECT].route: + return postProject(clientRequest, serverResponse); + + default: + /** + * In dev mode, we send the data to the console + * in other modes, we send the data to Sentry + */ + if (process.env.NODE_ENV === 'development') { + console.error('API route not found.'); + console.error({ + url: clientRequest.url, + query: clientRequest.query || {}, + body: clientRequest.body || {}, + error: 'API route not found.', + }); + } else { + /** + * Send to a log service + */ + } + return serverResponse.status(EHTTPErrorCode.CODE_404).send({ + error: { + code: EHTTPErrorCode.CODE_404, + message: 'API route not found.', + }, + } as IServerError); + } + } catch (error) { + /** + * In dev mode, we send the data to the console + * in other modes, we send the data to Sentry + */ + if (process.env.NODE_ENV === 'development') { + console.error(String(error)); + } else { + /** + * Send to a log service + */ + } + + return serverResponse.status(EHTTPErrorCode.CODE_500).send({ + error: { + code: EHTTPErrorCode.CODE_500, + message: 'API error', + }, + } as IServerError); + } +}; + +export default handler; diff --git a/pages/index.tsx b/pages/index.tsx new file mode 100644 index 0000000..98b3572 --- /dev/null +++ b/pages/index.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { NextPage } from 'next'; + +import { Layout } from '@uiComponents/Layout'; +import { ERouteType } from '@uiComponents/Layout/Layout'; +import { ApplicationHead } from '@uiComponents/ApplicationHead/ApplicationHead'; +import { Home } from '@views/Home'; + +export class SiteHomePage extends React.Component { + public render(): JSX.Element { + return ( + + + + + ); + } +} + +export default SiteHomePage; diff --git a/pages/post-profile.tsx b/pages/post-profile.tsx new file mode 100644 index 0000000..975a863 --- /dev/null +++ b/pages/post-profile.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { NextPage } from 'next'; + +import { Layout } from '@uiComponents/Layout'; +import { ERouteType } from '@uiComponents/Layout/Layout'; +import { ApplicationHead } from '@uiComponents/ApplicationHead/ApplicationHead'; +import { PostProfile } from '@views/Post'; + +export class PostProfilePage extends React.Component { + public render(): JSX.Element { + return ( + + + + + ); + } +} + +export default PostProfilePage; diff --git a/pages/post-project.tsx b/pages/post-project.tsx new file mode 100644 index 0000000..0437140 --- /dev/null +++ b/pages/post-project.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { NextPage } from 'next'; + +import { Layout } from '@uiComponents/Layout'; +import { ERouteType } from '@uiComponents/Layout/Layout'; +import { ApplicationHead } from '@uiComponents/ApplicationHead/ApplicationHead'; +import { PostProject } from '@views/Post/PostProject'; + +export class PostProjectPage extends React.Component { + public render(): JSX.Element { + return ( + + + + + ); + } +} + +export default PostProjectPage; diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index c2c86b859eaa20639adf92ff979c2be8d580433e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22382 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 QPNaAH_W=L@JAozeZ|l@fegFUf diff --git a/public/imgs/favicon.png b/public/imgs/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..4e2a5cdfccbe7c268d8505e76acfa3e50ff40008 GIT binary patch literal 1772 zcmbVNe@xU?94{yl*mO)XLoOuNN{l#q{c%6IHdKW>lw%$;I3sh$S=x78<*u!5xjURV zqFENhVy5B`K*u(MTTIj~2nl10V=S;_(P=g}x9LBVCHoPlMnsLW?e)03=;nUe3Wyq@D@~dJ$|F$Cg{Czs>(&5oD4JhU zLdif?Csjsi+Df8}eY_yarV)krT>&26032q<4X0?_M)@fU290&tpC&1mB&VT1MU(=~ ze}y_(%Ej8~2cR%%MBd>46)XyDK+(cHG+7Gs4S-PP1`JI(l65N~MT3H&cDDQXy>554 zrUaxAJkV=amZ9Eqx07O=PMo$8hvgc8aF}oxJZ+geEfylJ~{`d4IJg!6M{CkIZA1rkOo#g`~r{HYbXw z7Hm$eLJ}HJ7?u~zLf|mL%m5%_iEZhTJupoh4FX8|4|y>S)0Kb`+0KN4eGLL}j z6K2s)TSENV$oScP1)hY%u3;`y?FxRR8Xk~gm(yT7n>se2AxK)x>nZhD?7ZTCbJxeq zb6TQ%W|n8ne|JIV-24dles}Az|Bc%_rew^s^ko>ViF~T0c zJ2hvcvx~bvdDFplfBwZ!6WX9_SKoJ6ELr(;W+6L~FJ%uF9NiZA zq`+JA``Pth_jhmp;q~^6JfVHfh2_Y6Fnjbu{d+$~h9(9t$s=Tu{oF4%tKQywT0is3 m$b#0>fm)lVZ65xc<%O3Q6-~b5NMp?Zk+;0cbG&Th_CEk3n_->+ literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html deleted file mode 100644 index c240d2c..0000000 --- a/public/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - React App - - - -
- - - diff --git a/public/logo192.png b/public/logo192.png deleted file mode 100644 index fa313abf53936aefc517dbd583b724a57199d415..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8581 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�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 b!B~?t+WWKS5u5)CcMJGaRz{_UZlwPKNp@r} diff --git a/public/manifest.json b/public/manifest.json deleted file mode 100644 index 080d6c7..0000000 --- a/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "React App", - "name": "Create React App Sample", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/public/robot.txt b/public/robot.txt new file mode 100644 index 0000000..52c3d3a --- /dev/null +++ b/public/robot.txt @@ -0,0 +1,7 @@ +# Disallow +User-agent: Googlebot +Disallow: /app/ + +# Allow +User-agent: * +Allow: / diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index 01b0f9a..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..2c20ca2 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,11 @@ + + + + https://okfounder.com/ + 2020-12-14T10:04:33+00:00 + 1.00 + +​ + \ No newline at end of file diff --git a/src/App.js b/src/App.js deleted file mode 100644 index 57ef9cf..0000000 --- a/src/App.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react" -import { BrowserRouter as Router, Switch, Route } from "react-router-dom" -import { ThemeProvider, theme, CSSReset } from "@chakra-ui/core" -import LoginWrapper from "./auth/LoginWrapper" -import LogoutPage from "./auth/LogoutPage" -import Frame from "./ui/Frame" -import Home from "./Home" - -function App() { - return ( - - - - - {({ username, logout }) => ( - - - - - - - - - - - )} - - - - ) -} - -export default App diff --git a/src/Home.js b/src/Home.js deleted file mode 100644 index 0a2d19e..0000000 --- a/src/Home.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import Card from './ui/Card' -import DataAccessDemo from './features/DemoDataAccess' - -const Home = ({username}) => { - return - - -} - -export default Home - - - - \ No newline at end of file diff --git a/src/auth/LoginWrapper.js b/src/auth/LoginWrapper.js deleted file mode 100644 index c5d5f40..0000000 --- a/src/auth/LoginWrapper.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from "react" -import { - Box, - Flex, - Text, - Input, - Button, - FormControl, - FormLabel, - FormHelperText -} from "@chakra-ui/core" -import Header from "../ui/Header" - -export default class LoginWrapper extends React.Component { - state = { username: localStorage.getItem("username") || null } - - login = username => { - localStorage.setItem("username", username) - this.setState({ username: username }) - } - - logout = () => { - localStorage.removeItem("username") - this.setState({ username: undefined }) - } - - render() { - let { username } = this.state - let inputRef = React.createRef() - // If we are logged in, then pass the username to the children. - if (username) { - return this.props.children({ username, logout: this.logout }) - } - - // Otherwise, show a login form. - return ( - - -

- - - - Username - - - Your email, or literally anything - - - - - - - - - ) - } -} diff --git a/src/auth/LogoutPage.js b/src/auth/LogoutPage.js deleted file mode 100644 index d3398c4..0000000 --- a/src/auth/LogoutPage.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react" -import { Redirect } from "react-router-dom" - - -export default class LogoutPage extends React.Component { - - componentDidMount() { - if (this.props.logout) { - this.props.logout() - } - } - - render() { - return - } - -} - diff --git a/src/data/database.js b/src/data/database.js deleted file mode 100644 index ca9fc36..0000000 --- a/src/data/database.js +++ /dev/null @@ -1,30 +0,0 @@ -import localStorageDB from "localstoragedb" -// Uses https://github.com/knadh/localStorageDB - -// Initialise. If the database doesn't exist, it is created - -//////////////////////////////////////////////// -// Change the dbName if you change the schema or -// initial data to reset the db -const dbName = "userData_2" -///////////////////////////////////////////////// - -var db = new localStorageDB(dbName, localStorage); - -// Check if the database was just created. Useful for initial database setup -if( !db.tableExists("posts")) { - - // create the "posts" table - db.createTable("posts", ["user", "title"]); - - // insert some data - db.insert("posts", {user: "billgates", title: "Microsoft is great"}); - db.insert("posts", {user: "billgates", title: "It looks like you're writing a letter"}); - db.insert("posts", {user: "stevejobs", title: "It just works"}); - - // commit the database to localStorage - // all create/drop/insert/update/delete operations should be committed - db.commit(); -} - -export default db \ No newline at end of file diff --git a/src/features/DemoDataAccess.js b/src/features/DemoDataAccess.js deleted file mode 100644 index 7598fe1..0000000 --- a/src/features/DemoDataAccess.js +++ /dev/null @@ -1,79 +0,0 @@ -import React from "react" -import { - Text, - FormControl, - Input, - FormLabel, - Button, - Box -} from "@chakra-ui/core" -import db from "../data/database" -import Card from "../ui/Card" - -export default class DataAccessDemo extends React.Component { - /* - - This is a demo component to show you how to use the localStorage database - - */ - - - state = { posts: null } - inputRef = React.createRef() - - createPost = () => { - const title = this.inputRef.current.value - if (!title) return - const post = { - title: title, - user: this.props.username - } - db.insert("posts", post) - db.commit() - this.clearInput() - this.fetchUserPosts() - } - - clearInput = () => { - this.inputRef.current.value = null - } - - componentDidMount() { - this.fetchUserPosts() - - } - - fetchUserPosts() { - const posts = db.queryAll("posts", { - query: { user: this.props.username } - }) - this.setState({ posts }) - } - - renderPosts() { - const { posts } = this.state - if (!posts) return No posts yet - return posts.map((post, index) => ( - - )) - } - - render() { - return ( - <> - - - Create a new post - - - - - - - {this.renderPosts()} - - ) - } -} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 0fc5642..0000000 --- a/src/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom" -import App from "./App" -ReactDOM.render(, document.getElementById("root")) diff --git a/src/ui/Card.js b/src/ui/Card.js deleted file mode 100644 index f87532f..0000000 --- a/src/ui/Card.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react' -import { Box, Image, Flex, Badge, Text, Button, StarIcon, Icon } from "@chakra-ui/core"; - - -const Card = ({title, author}) => { - - let rating = 3 - let count = 42 - return ( - - - - - - New - - - - - {title} - - - - - By{" "} - - {author} - - - - {Array(5) - .fill("") - .map((_, i) => ( - - ))} - - {count} reviews - - - - - )} - - export default Card \ No newline at end of file diff --git a/src/ui/Frame.js b/src/ui/Frame.js deleted file mode 100644 index 8722cd1..0000000 --- a/src/ui/Frame.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react" -import { Box, Flex, Text } from "@chakra-ui/core" -import { Link } from "react-router-dom" -import Header from "./Header" - -const Frame = ({ children, username }) => { - return ( - - -
- - Hello {username}! Log Out - -
- {children} -
-
- ) -} -export default Frame diff --git a/src/ui/Header.js b/src/ui/Header.js deleted file mode 100644 index c86674e..0000000 --- a/src/ui/Header.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' -import { Box, Flex, Link, Text } from "@chakra-ui/core" -import Logo from "./Logo" - -const Header = ({children}) => { - - return - - - {children} - - - - - -} - -export default Header \ No newline at end of file diff --git a/src/ui/Logo.js b/src/ui/Logo.js deleted file mode 100644 index 3ed421d..0000000 --- a/src/ui/Logo.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react" -import { Heading, Icon } from "@chakra-ui/core" - -const Logo = () => { - return - OkFounder - -} - -export default Logo \ No newline at end of file diff --git a/stylelint.config.js b/stylelint.config.js new file mode 100644 index 0000000..03f3ca2 --- /dev/null +++ b/stylelint.config.js @@ -0,0 +1,14 @@ +module.exports = { + extends: 'stylelint-config-standard', + ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], + rules: { + 'max-empty-lines': 2, + 'value-list-comma-newline-after': null, + 'declaration-colon-newline-after': null, + 'no-descending-specificity': null, + 'rule-empty-line-before': [ + 'always', + { ignore: ['after-comment', 'first-nested', 'inside-block'] }, + ], + }, +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5207df5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,44 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "module": "esnext", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, + + /* Additional Checks */ + "noUnusedLocals": true /* Report errors on unused locals. */, + "noUnusedParameters": true /* Report errors on unused parameters. */, + "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, + "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, + + /* Module Resolution Options */ + "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + "baseUrl": "." /* Base directory to resolve non-absolute module names */, + "paths": { + "@apiFunctions/*": ["apiFunctions/*"], + "@constants/*": ["./constants/*"], + "@pages/*": ["./pages/*"], + "@redirects/*": ["./redirects/*"], + "@customTypes/*": ["customTypes/*"], + "@uiAssets/*": ["./uiAssets/*"], + "@uiComponents/*": ["./uiComponents/*"], + "@utils/*": ["./utils/*"], + "@views/*": ["./views/*"], + "@root/*": ["./*"] + } /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + }, + "exclude": ["node_modules", "/**/*.cy.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] +} diff --git a/uiComponents/ApplicationHead/ApplicationHead.tsx b/uiComponents/ApplicationHead/ApplicationHead.tsx new file mode 100644 index 0000000..2f96035 --- /dev/null +++ b/uiComponents/ApplicationHead/ApplicationHead.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import Head from 'next/head'; + +export interface IHeadParams { + canonicalPath?: string; + description?: string; + hiddenPage?: boolean; + keywords?: Array; + language?: string; + opengraphImagePath?: string; + siteName?: string; + title?: string; +} + +export const ApplicationHead = ({ + canonicalPath = '/', + description = 'Find the rigght co founder!', + hiddenPage, + keywords = [], + language = 'en-US', + opengraphImagePath = '/imgs/home/og-image.jpg', + siteName = 'okfounder', + title = 'okfounder, find the right cofoudner', +}: IHeadParams): JSX.Element => ( + + + + + + + + + + {title} + + + + + + + + + + + + + + + + + + + + +); diff --git a/uiComponents/Button/Button.pcss b/uiComponents/Button/Button.pcss new file mode 100644 index 0000000..d301087 --- /dev/null +++ b/uiComponents/Button/Button.pcss @@ -0,0 +1,124 @@ +.__SCOPE { + align-items: center; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + letter-spacing: 0.25px; + line-height: 1; + max-width: 100%; + overflow: hidden; + padding: 0 var(--spacing-small); + position: relative; + text-decoration: none; + height: var(--spacing-finger); + border: 2px solid #00000066; + width: 100%; + background: none; + border-radius: var(--spacing-small); + text-decoration: none; + white-space: nowrap; + text-align: center; + + &:hover { + text-decoration: none; + } + + &.baseButton { + background-color: var(--colors-main); + transition: background-color 200ms ease-out, border 200ms ease-out, + box-shadow 200ms ease-out; + box-shadow: var(--shadow-lighter); + + &:hover { + box-shadow: var(--shadow-light); + } + } + + &.secondary { + transition: background-color 200ms ease-out, border 200ms ease-out, + box-shadow 200ms ease-out; + box-shadow: var(--shadow-lighter); + + &:hover { + box-shadow: var(--shadow-light); + } + } + + &.small { + border-radius: var(--spacing-xs); + height: var(--spacing-regular); + font-size: var(--typography-size-f1); + padding: 0 var(--spacing-small); + + > .children { + > .label { + font-size: var(--typography-size-f1); + } + + > .icon { + height: 20px; + width: 20px; + } + } + } + + > .children { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + + > .icon { + background-color: rgba(0, 0, 0, 0.1); + border-radius: 100px; + height: 28px; + width: 28px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + > svg { + display: block; + } + } + + > .noIcon { + flex-shrink: 0; + width: 28px; + display: flex; + } + + > .label { + flex-grow: 1; + display: block; + font-size: var(--typography-size-f3); + font-weight: var(--typography-weight-normal); + padding: 0 var(--spacing-small); + } + } + + &.disabled { + opacity: 0.5; + } + + &.disabled, + &.isLoading { + box-shadow: none; + cursor: not-allowed; + + &:hover { + box-shadow: none; + transform: none; + } + } + + &.link { + display: inline-block; + } + + &:focus { + outline: none; + } +} diff --git a/uiComponents/Button/Button.tsx b/uiComponents/Button/Button.tsx new file mode 100644 index 0000000..635ca4a --- /dev/null +++ b/uiComponents/Button/Button.tsx @@ -0,0 +1,195 @@ +import React from 'react'; +import Styled from 'react-styles-injector'; + +import { Loader } from '@uiComponents/Loader'; +import { Icon } from '@uiComponents/Icon'; +import { EAppColors } from '@constants/appColors'; + +import styles from './Button.pcss'; +import Link from 'next/link'; + +export interface IProps { + readonly background?: string; + readonly children?: React.ReactNode; + readonly className?: string; + readonly color?: string; + readonly disabled?: boolean; + readonly formAction?: string; + readonly href?: string; + readonly iconLeft?: string; + readonly iconRight?: string; + readonly id?: string; + readonly isLoading?: boolean; + readonly label?: string; + readonly name?: string; + readonly onClick?: (event: React.MouseEvent) => void; + readonly onMouseDown?: (event: React.MouseEvent) => void; + readonly onMouseOver?: (event: React.MouseEvent) => void; + readonly onMouseUp?: (event: React.MouseEvent) => void; + readonly outerHref?: string; + readonly size?: 'small' | 'big'; + readonly style?: React.CSSProperties; + readonly type?: 'button' | 'submit' | 'reset'; + readonly value?: string | number; + readonly width?: number | string; + readonly secondary?: boolean; + readonly tertiary?: boolean; +} + +const ButtonComponent: React.FC = ({ + background, + children, + className, + color, + disabled, + formAction, + href, + iconLeft, + iconRight, + id, + isLoading, + label, + name, + onClick, + onMouseDown, + onMouseOver, + onMouseUp, + outerHref, + size, + style, + type, + value, + width, + secondary, + tertiary, +}: IProps) => { + const stylesToInline: React.CSSProperties = { + ...style, + }; + + if (width && typeof width === 'number') { + stylesToInline.width = `${width}px`; + } else if (width && typeof width === 'string') { + stylesToInline.width = width; + } + + let computedClassName = `${styles.hash} ${size || 'normal'}`; + if (className) { + computedClassName += ' ' + className; + } + if (disabled) { + computedClassName += ' disabled'; + } + if (isLoading) { + computedClassName += ' isLoading'; + } + if (!secondary && !tertiary) { + computedClassName += ' baseButton'; + stylesToInline.color = color || EAppColors.WHITE; + stylesToInline.background = background || EAppColors.PRIMARY; + } else if (secondary) { + computedClassName += ' secondary'; + stylesToInline.color = color || EAppColors.PRIMARY; + stylesToInline.background = background || EAppColors.WHITE; + } else if (tertiary) { + computedClassName += ' tertiary'; + stylesToInline.color = color || EAppColors.PRIMARY; + } + + const content = ( + + {isLoading ? ( + + ) : ( + + {iconLeft && ( + + + + )} + {label && {label}} + {iconRight && ( + + + + )} + {children} + + )} + + ); + + if (href && !disabled) { + return ( + + + + {content} + + + + ); + } + + if (outerHref && !disabled) { + return ( + + + {content} + + + ); + } + + return ( + + + + ); +}; + +export const Button = React.memo(ButtonComponent); diff --git a/uiComponents/Button/index.ts b/uiComponents/Button/index.ts new file mode 100644 index 0000000..fe9c53c --- /dev/null +++ b/uiComponents/Button/index.ts @@ -0,0 +1 @@ +export { Button } from './Button'; diff --git a/uiComponents/FullHeightContainer/FullHeightContainer.tsx b/uiComponents/FullHeightContainer/FullHeightContainer.tsx new file mode 100644 index 0000000..13c2383 --- /dev/null +++ b/uiComponents/FullHeightContainer/FullHeightContainer.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { debounce } from '@utils/toolBelt/debounce'; + +const getWindowHeight = (): string => + ((document && + document.documentElement && + document.documentElement.clientHeight) || + window.innerHeight) + 'px'; + +export const FullHeightContainer: React.FC = ({ children }): JSX.Element => { + if (typeof window === 'undefined') { + return {children}; + } + + const [windowHeight, setWindowHeight] = React.useState(getWindowHeight()); + + React.useEffect(() => { + const handleResize = debounce(() => { + setWindowHeight(getWindowHeight()); + }, 100); + + window.addEventListener('resize', handleResize); + + return (): void => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return
{children}
; +}; diff --git a/uiComponents/FullHeightContainer/index.ts b/uiComponents/FullHeightContainer/index.ts new file mode 100644 index 0000000..e2b1f00 --- /dev/null +++ b/uiComponents/FullHeightContainer/index.ts @@ -0,0 +1 @@ +export { FullHeightContainer } from './FullHeightContainer'; diff --git a/uiComponents/GlobalStyles/Animations.pcss b/uiComponents/GlobalStyles/Animations.pcss new file mode 100644 index 0000000..44f6c59 --- /dev/null +++ b/uiComponents/GlobalStyles/Animations.pcss @@ -0,0 +1,62 @@ +@keyframes slideRightAnimateIn { + 0% { + transform: translateX(30px); + opacity: 0; + } + 75% { + transform: translateX(-5px); + } + 100% { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideLeftAnimateIn { + 0% { + transform: translateX(-30px); + opacity: 0; + } + 75% { + transform: translateX(5px); + } + 100% { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideBottomAnimateIn { + 0% { + transform: translateY(20px); + opacity: 0; + } + 50% { + transform: translateY(-5px); + } + 100% { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes scaleAnimateIn { + 0% { + transform: scale(0); + } + 75% { + transform: scale(1.25); + } + 100% { + transform: scale(1); + } +} + +@keyframes opacityAnimateIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/uiComponents/GlobalStyles/Constants.pcss b/uiComponents/GlobalStyles/Constants.pcss new file mode 100644 index 0000000..9306888 --- /dev/null +++ b/uiComponents/GlobalStyles/Constants.pcss @@ -0,0 +1,123 @@ +:root { + /* ********** */ + /* Colors */ + /* ********** */ + /* Semantic colors */ + --colors-success: #69bfa0; + --colors-successLight: #b8e5d3; + --colors-successLighter: #dff2ea; + --colors-successDark: #008055; + --colors-successDarker: #006647; + --colors-successText: #191919; + --colors-warning: #e8a126; + --colors-warningLight: #ffdea6; + --colors-warningLighter: #fff1d9; + --colors-warningDark: #cc8100; + --colors-warningDarker: #994d00; + --colors-warningText: #191919; + --colors-error: #e58989; + --colors-errorLight: #f2cece; + --colors-errorLighter: #faebeb; + --colors-errorDark: #b20000; + --colors-errorDarker: #900; + --colors-errorText: #191919; + /* Ui colors */ + --colors-primary: #77d4c0; + --colors-primaryFlash: #00e3ae; + --colors-primaryLight: #a0f0d8; + --colors-primaryText: #546681; + --colors-primaryTextLight: #4a5e92; + --colors-secondaryText: #1a3066; + --colors-primaryBg: #49fefe66; + --colors-primaryDark: #0f4325; + --colors-bg: #0c0f14; + --colors-bgBg: #0c0f14aa; + --colors-accent: #fdbb2d; + --colors-accentBg: #b1761e66; + --colors-accentLight: #feffdb; + --colors-accentDark: #907a0d; + --colors-white: #fff; + --colors-black: #333; + --colors-grey1: #666; + --colors-grey2: #999; + --colors-grey3: #ccc; + --colors-grey4: #eee; + --colors-grey5: #f6f6f6; + /* Gradients */ + --colors-gradient-light: linear-gradient( + 0deg, + rgba(240, 247, 254, 1) 0%, + rgba(255, 255, 255, 1) 100% + ); + --colors-gradient-strong: linear-gradient( + 0deg, + rgba(91, 192, 175, 1) 0%, + rgba(160, 240, 216, 1) 100% + ); + + /* ********** */ + /* Typography */ + /* ********** */ + --typography-weight-light: 300; + --typography-weight-normal: 400; + --typography-weight-medium: 600; + --typography-weight-bold: 700; + --typography-size-f8: 72px; + --typography-size-f7: 48px; + --typography-size-f6: 32px; + --typography-size-f5: 24px; + --typography-size-f4: 20px; + --typography-size-f3: 18px; + --typography-size-f2: 16px; + --typography-size-f1: 14px; + --typography-size-f0: 12px; + --typography-size-fb: 10px; + + /* ********** */ + /* Spaces */ + /* ********** */ + --spacing-none: 0; + --spacing-xs: 4px; + --spacing-small: 8px; + --spacing-tight: 16px; + --spacing-tightLooser: 24px; + --spacing-regular: 32px; + --spacing-finger: 48px; + --spacing-loose: 64px; + --spacing-large: 128px; + --spacing-xl: 256px; + --spacing-xxl: 512px; + + /* ********** */ + /* Shadows */ + /* ********** */ + --shadow-base: rgba(0, 0, 0, 0.25) 0 6px 10px; + --shadow-light: rgba(0, 0, 0, 0.15) 0 6px 10px; + --shadow-lighter: rgba(0, 0, 0, 0.1) 0 6px 10px; + --shadow-lighterer: rgba(0, 0, 0, 0.05) 0 6px 10px; + --shadow-base-small: rgba(0, 0, 0, 0.25) 0 3px 5px; + --shadow-light-small: rgba(0, 0, 0, 0.15) 0 3px 5px; + --shadow-lighter-small: rgba(0, 0, 0, 0.1) 0 3px 5px; + --shadow-lighterer-small: rgba(0, 0, 0, 0.05) 0 3px 5px; + --shadow-base-large: rgba(0, 0, 0, 0.25) 0 12px 20px; + --shadow-light-large: rgba(0, 0, 0, 0.15) 0 12px 20px; + --shadow-lighter-large: rgba(0, 0, 0, 0.1) 0 12px 20px; + --shadow-lighterer-large: rgba(0, 0, 0, 0.05) 0 12px 20px; + --shadow-base-xlarge: rgba(0, 0, 0, 0.25) 0 24px 40px; + --shadow-light-xlarge: rgba(0, 0, 0, 0.15) 0 24px 40px; + --shadow-lighter-xlarge: rgba(0, 0, 0, 0.1) 0 24px 40px; + --shadow-lighterer-xlarge: rgba(0, 0, 0, 0.05) 0 24px 40px; + + /* ********** */ + /* Background patterns */ + /* see https://jsbin.com/pudujif/embed to build more */ + /* ********** */ + /* */ + /* */ + /* */ + --pattern-smallWhiteDots: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1IiBoZWlnaHQ9IjUiPgo8cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjEiPjwvcmVjdD4KPC9zdmc+'); + /* */ + /* */ + /* */ + --pattern-slashes: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1IiBoZWlnaHQ9IjEwIj4KPHBhdGggZD0iTTAsMTBMNCwwTDUsMEwxLDEwWiIgZmlsbD0iIzJhYTJhMiIgb3BhY2l0eT0iMSI+PC9wYXRoPgo8L3N2Zz4='); +} diff --git a/uiComponents/GlobalStyles/GlobalStyles.pcss b/uiComponents/GlobalStyles/GlobalStyles.pcss new file mode 100644 index 0000000..59207ba --- /dev/null +++ b/uiComponents/GlobalStyles/GlobalStyles.pcss @@ -0,0 +1,120 @@ +* { + box-sizing: border-box; + padding: 0; + margin: 0; + font: inherit; + vertical-align: baseline; + color: inherit; + touch-action: manipulation; + + &:focus { + outline: none; + } +} + +html { + overscroll-behavior: none; + overflow: hidden; + height: 100%; +} + +body { + font-size: var(--typography-size-f2); + line-height: 1.5; + font-family: 'IBM Plex Sans', sans-serif; + font-family: 'IBM Plex Mono', monospace; + text-rendering: optimizeLegibility; + word-wrap: break-word; + overscroll-behavior: none; + overflow-x: hidden; + color: var(--colors-primaryText); + background: #f6f6f6; + height: 100%; + overflow: auto; +} + +#__next { + height: fill-available; +} + +a, +button { + cursor: pointer; + touch-action: manipulation; +} + +input { + user-select: initial; + touch-action: manipulation; +} + +b { + font-weight: 700; +} + +i { + font-style: italic; +} + +::-moz-selection { + color: var(--colors-white); + background: var(--colors-primary); +} + +::selection { + color: var(--colors-white); + background: var(--colors-primary); +} + +h1 { + font-size: var(--typography-size-f6); + font-weight: var(--typography-weight-normal); + color: var(--colors-secondaryText); +} +h2 { + font-size: var(--typography-size-f5); + font-weight: var(--typography-weight-normal); + color: var(--colors-secondaryText); +} +h3 { + font-size: var(--typography-size-f4); + font-weight: var(--typography-weight-normal); + color: var(--colors-secondaryText); +} +h4 { + font-size: var(--typography-size-f3); + font-weight: var(--typography-weight-normal); + color: var(--colors-secondaryText); +} + +@media (min-width: 700px) { + h1 { + font-size: var(--typography-size-f7); + } + h2 { + font-size: var(--typography-size-f6); + } + h3 { + font-size: var(--typography-size-f5); + } + h4 { + font-size: var(--typography-size-f4); + } +} + +.strong, +strong { + font-weight: var(--typography-weight-bold); +} + +.small { + font-size: var(--typography-size-f1); +} + +.caption { + font-size: var(--typography-size-f0); +} + +em { + font-style: italic; +} diff --git a/uiComponents/GlobalStyles/GlobalStyles.tsx b/uiComponents/GlobalStyles/GlobalStyles.tsx new file mode 100644 index 0000000..aabc7ee --- /dev/null +++ b/uiComponents/GlobalStyles/GlobalStyles.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import Styled from 'react-styles-injector'; + +import styles from './GlobalStyles.pcss'; +import Constants from './Constants.pcss'; +import Animations from './Animations.pcss'; + +export const GlobalStyles = (): JSX.Element => ( + +); diff --git a/uiComponents/GlobalStyles/index.ts b/uiComponents/GlobalStyles/index.ts new file mode 100644 index 0000000..c10d48e --- /dev/null +++ b/uiComponents/GlobalStyles/index.ts @@ -0,0 +1 @@ +export { GlobalStyles } from './GlobalStyles'; diff --git a/uiComponents/Icon/Icon.tsx b/uiComponents/Icon/Icon.tsx new file mode 100644 index 0000000..51a7a7c --- /dev/null +++ b/uiComponents/Icon/Icon.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +export interface IIconProps { + readonly className?: string; + readonly height?: number; + readonly icon: string; + readonly color?: string; + readonly style?: React.CSSProperties; + readonly width?: number; +} + +const IconComponent: React.SFC = ({ + className, + height, + icon = '', + color, + style, + width = 24, +}: IIconProps) => { + const styles: React.CSSProperties = { + fill: color || '#666', + stroke: 'none', + transition: 'fill .2s ease-out, transform .2s ease-out', + ...style, + }; + + return ( + + + + ); +}; + +export const Icon = React.memo(IconComponent); diff --git a/uiComponents/Icon/index.ts b/uiComponents/Icon/index.ts new file mode 100644 index 0000000..3f20921 --- /dev/null +++ b/uiComponents/Icon/index.ts @@ -0,0 +1 @@ +export { Icon } from './Icon'; diff --git a/uiComponents/Input/Input.pcss b/uiComponents/Input/Input.pcss new file mode 100644 index 0000000..e70bf58 --- /dev/null +++ b/uiComponents/Input/Input.pcss @@ -0,0 +1,78 @@ +.__SCOPE { + display: block; + cursor: pointer; + + &.disabled * { + cursor: not-allowed; + opacity: 0.75; + } + + > .topLabel { + font-size: var(--typography-size-f0); + font-weight: var(--typography-weight-bold); + line-height: 1; + text-transform: none; + padding: 12px var(--spacing-tight) 0 var(--spacing-tight); + display: flex; + align-items: center; + background-color: var(--colors-white); + } + + > .subLabel { + font-size: var(--typography-size-f1); + margin-top: var(--spacing-small); + display: flex; + align-items: center; + + > svg { + display: block; + margin-right: 3px; + } + } + + > .inputContainer { + position: relative; + background: var(--colors-white); + + > .loaderContainer { + position: relative; + height: var(--spacing-finger); + align-items: flex-start; + padding: 0 var(--spacing-tight); + } + + > input, + > textarea { + cursor: text; + background: none; + display: block; + font-size: var(--typography-size-f2); + padding: 0 var(--spacing-tight); + resize: none; + transition: background-color 200ms ease-out, border 200ms ease-out; + width: 100%; + line-height: 1; + height: var(--spacing-finger); + border: none; + + &::placeholder { + color: #aaa; + } + } + + > textarea { + padding: 8px var(--spacing-tight); + height: auto; + } + + > svg { + left: 15px; + position: absolute; + top: 15px; + } + } + + &.hasIcon > div > input { + padding-left: 43px; + } +} diff --git a/uiComponents/Input/Input.tsx b/uiComponents/Input/Input.tsx new file mode 100644 index 0000000..2380955 --- /dev/null +++ b/uiComponents/Input/Input.tsx @@ -0,0 +1,212 @@ +import React from 'react'; +import Styled from 'react-styles-injector'; + +import { mdiAlert, mdiAlertOctagon } from '@mdi/js'; +import { Icon } from 'uiComponents/Icon'; + +import styles from './Input.pcss'; +import { EAppColors } from '@constants/appColors'; +import { Loader } from '@uiComponents/Loader'; + +export interface IProps { + readonly autoComplete?: string; + readonly className?: string; + readonly defaultValue?: string | Date | number; + readonly disabled?: boolean; + readonly error?: string | React.ReactNode; + readonly id?: string; + readonly isLoading?: boolean; + readonly label: string | React.ReactNode; + readonly max?: string | number; + readonly maxLength?: number; + readonly min?: string | number; + readonly minLength?: number; + readonly name?: string; + readonly onBlur?: ( + event: React.FormEvent, + value?: string, + ) => void | Promise; + readonly onChange?: ( + event: React.FormEvent, + value?: string, + ) => void | Promise; + readonly onFocus?: ( + event: React.FormEvent, + value?: string, + ) => void | Promise; + readonly onKeyDown?: ( + event: React.KeyboardEvent, + value?: string, + ) => void | Promise; + readonly pattern?: string; + readonly placeholder?: string; + readonly readonly?: boolean; + readonly required?: boolean; + readonly step?: string; + readonly sublabel?: string | React.ReactNode; + readonly type?: + | 'text' + | 'date' + | 'textarea' + | 'email' + | 'color' + | 'number' + | 'password' + | 'range' + | 'search' + | 'tel' + | string; + readonly warning?: string | React.ReactNode; +} + +let inputIndex = 0; + +export class Input extends React.Component { + private input!: HTMLInputElement | HTMLTextAreaElement; + + private onFocus = ( + event: React.FormEvent, + ): void => { + if (this.props.onFocus && typeof this.props.onFocus === 'function') { + this.props.onFocus(event, event.currentTarget.value); + } + }; + + private onBlur = ( + event: React.FormEvent, + ): void => { + if (this.props.onBlur && typeof this.props.onBlur === 'function') { + this.props.onBlur(event, event.currentTarget.value); + } + }; + + private onChange = ( + event: React.FormEvent, + ): void => { + if (this.props.onChange && typeof this.props.onChange === 'function') { + this.props.onChange(event, event.currentTarget.value); + } + }; + + private onKeyDown = (event: React.KeyboardEvent) => { + if (this.props.onKeyDown && typeof this.props.onKeyDown === 'function') { + this.props.onKeyDown(event, event.currentTarget.value); + } + }; + + public focusInput = (): void => this.input.focus(); + + public getValue = (): string => this.input.value; + + public setValue = (value: string): string => (this.input.value = value); + + public render(): JSX.Element { + const { + autoComplete, + className = '', + disabled, + error, + id, + isLoading, + label, + max, + maxLength, + min, + minLength, + name, + pattern, + placeholder, + readonly, + required, + step, + sublabel, + type = 'text', + warning, + } = this.props; + let { defaultValue } = this.props; + + inputIndex++; + const uniqId = `inputId-${inputIndex}`; + + if (type === 'date' && defaultValue) { + const d = new Date(defaultValue); + defaultValue = `${d.getFullYear()}-${`0${d.getMonth() + 1}`.slice( + -2, + )}-${`0${d.getDate()}`.slice(-2)}`; + } + defaultValue = defaultValue ? String(defaultValue) : ''; + + return ( + + {label &&
{label}
} +
+ {isLoading && } + + {!isLoading && type === 'textarea' && ( +