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 c2c86b8..0000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/imgs/favicon.png b/public/imgs/favicon.png new file mode 100644 index 0000000..4e2a5cd Binary files /dev/null and b/public/imgs/favicon.png differ 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 fa313ab..0000000 Binary files a/public/logo192.png and /dev/null differ diff --git a/public/logo512.png b/public/logo512.png deleted file mode 100644 index bd5d4b5..0000000 Binary files a/public/logo512.png and /dev/null differ 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' && ( +