diff --git a/client/.babelrc b/client/.babelrc index f45bd23..aa70268 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -1,9 +1,18 @@ { - "presets": ["@babel/preset-env", "@babel/react"], + "presets": [ + [ + "@babel/preset-env", + { + "useBuiltIns": "entry", + "corejs": 3 + } + ], + "@babel/react" + ], "plugins": [ ["@babel/plugin-proposal-decorators", { "legacy": true }], "@babel/plugin-proposal-export-namespace-from", "@babel/plugin-syntax-dynamic-import", - ["@babel/plugin-proposal-class-properties", { "loose": true }], + ["@babel/plugin-proposal-class-properties", { "loose": true }] ] } diff --git a/client/.eslintrc.json b/client/.eslintrc.json index 8f26a99..f2469b4 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -12,7 +12,10 @@ "jest": true }, "extends": ["airbnb", "prettier", "prettier/react"], + "plugins": ["react-hooks"], "rules": { + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", "radix": 0, "no-restricted-syntax": 0, "no-await-in-loop": 0, @@ -25,10 +28,12 @@ "import/no-cycle": 0, "react/no-array-index-key": 0, "react/forbid-prop-types": 0, + "react/state-in-constructor": 0, + "react/jsx-props-no-spreading": 0, "jsx-a11y/click-events-have-key-events": 0 }, "settings": { - // Allows us to use absolute imports within codebase + // Allows us to lint absolute "src" imports within codebase "import/resolver": { "node": { "moduleDirectory": ["node_modules", "src/"] diff --git a/client/jsconfig.json b/client/jsconfig.json new file mode 100644 index 0000000..6835f58 --- /dev/null +++ b/client/jsconfig.json @@ -0,0 +1,8 @@ +// This config allows VSCode intellisense to work with absolute "src" imports and jsx files +{ + "compilerOptions": { + "baseUrl": "./src", + "jsx": "react" + }, + "exclude": ["node_modules", "dist", "dev"] +} diff --git a/client/package-lock.json b/client/package-lock.json index 130a964..f044fd1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2432,6 +2432,11 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, + "core-js": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.4.7.tgz", + "integrity": "sha512-qaPVGw30J1wQ0GR3GvoPqlGf9GZfKKF4kFC7kiHlcsPTqH3txrs9crCp3ZiMAXuSenhz89Jnl4GZs/67S5VOSg==" + }, "core-js-compat": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.4.7.tgz", @@ -2598,6 +2603,11 @@ "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", "dev": true }, + "csstype": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz", + "integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==" + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -2841,6 +2851,15 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.3.tgz", + "integrity": "sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw==", + "requires": { + "@babel/runtime": "^7.6.3", + "csstype": "^2.6.7" + } + }, "dom-serializer": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", @@ -7661,6 +7680,17 @@ "prop-types": "^15.6.0" } }, + "react-transition-group": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz", + "integrity": "sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", diff --git a/client/package.json b/client/package.json index f7c07af..ad9cae1 100644 --- a/client/package.json +++ b/client/package.json @@ -37,6 +37,7 @@ "dependencies": { "axios": "^0.19.0", "color": "^3.1.2", + "core-js": "^3.4.7", "history": "^4.10.1", "jwt-decode": "^2.2.0", "lodash": "^4.17.15", @@ -47,6 +48,7 @@ "react-dom": "^16.12.0", "react-router-dom": "^5.1.2", "react-textarea-autosize": "^7.1.2", + "react-transition-group": "^4.3.0", "styled-components": "^4.4.1", "sweet-pubsub": "^1.1.2" }, diff --git a/client/src/browserHistory.js b/client/src/browserHistory.js new file mode 100644 index 0000000..9937105 --- /dev/null +++ b/client/src/browserHistory.js @@ -0,0 +1,3 @@ +import { createBrowserHistory } from 'history'; + +export default createBrowserHistory(); diff --git a/client/src/components/App/App.jsx b/client/src/components/App/App.jsx new file mode 100644 index 0000000..5d67ec9 --- /dev/null +++ b/client/src/components/App/App.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import Toast from './Toast'; +import Routes from './Routes'; +import NormalizeStyles from './NormalizeStyles'; +import FontStyles from './FontStyles'; +import BaseStyles from './BaseStyles'; + +const App = () => ( + <> + + + + + + +); + +export default App; diff --git a/client/src/components/App/AppStyles.js b/client/src/components/App/AppStyles.js new file mode 100644 index 0000000..67814ef --- /dev/null +++ b/client/src/components/App/AppStyles.js @@ -0,0 +1,6 @@ +import styled from 'styled-components'; + +export const Main = styled.main` + width: 100%; + padding-left: 75px; +`; diff --git a/client/src/components/App/BaseStyles.js b/client/src/components/App/BaseStyles.js new file mode 100644 index 0000000..6074be1 --- /dev/null +++ b/client/src/components/App/BaseStyles.js @@ -0,0 +1,108 @@ +import { createGlobalStyle } from 'styled-components'; + +import { color, font, mixin } from 'shared/utils/styles'; + +export default createGlobalStyle` + html, body, #root { + height: 100%; + min-height: 100%; + } + + body { + color: ${color.textDarkest}; + -webkit-tap-highlight-color: transparent; + line-height: 1.2; + ${font.size(16)} + ${font.regular} + } + + #root { + display: flex; + flex-direction: column; + } + + button, + input, + optgroup, + select, + textarea { + ${font.regular} + } + + *, *:after, *:before, input[type="search"] { + box-sizing: border-box; + } + + a, a:hover, a:visited, a:active { + text-decoration: none; + } + + ul { + list-style: none; + } + + ul, li, ol, dd, h1, h2, h3, h4, h5, h6, p { + padding: 0; + margin: 0; + } + + h1, h2, h3, h4, h5, h6, strong { + ${font.bold} + } + + button { + background: none; + border: none; + } + + /* Workaround for IE11 focus highlighting for select elements */ + select::-ms-value { + background: none; + color: #42413d; + } + + [role="button"], button, input, select, textarea { + outline: none; + &:focus { + outline: none; + } + &:disabled { + opacity: 1; + } + } + [role="button"], button, input, textarea { + appearance: none; + } + select:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #000; + } + select::-ms-expand { + display: none; + } + select option { + color: ${color.textDarkest}; + } + + p { + line-height: 1.6; + a { + ${mixin.link()} + } + } + + textarea { + line-height: 1.6; + } + + body, select { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + html { + touch-action: manipulation; + } + + ${mixin.placeholderColor(color.textLightBlue)} +`; diff --git a/client/src/components/App/FontStyles.js b/client/src/components/App/FontStyles.js new file mode 100644 index 0000000..b74d56e --- /dev/null +++ b/client/src/components/App/FontStyles.js @@ -0,0 +1,53 @@ +import { createGlobalStyle } from 'styled-components'; + +import BlackWoff2 from 'shared/assets/fonts/CircularStd-Black.woff2'; +import BoldWoff2 from 'shared/assets/fonts/CircularStd-Bold.woff2'; +import MediumWoff2 from 'shared/assets/fonts/CircularStd-Medium.woff2'; +import BookWoff2 from 'shared/assets/fonts/CircularStd-Book.woff2'; +import BlackWoff from 'shared/assets/fonts/CircularStd-Black.woff'; +import BoldWoff from 'shared/assets/fonts/CircularStd-Bold.woff'; +import MediumWoff from 'shared/assets/fonts/CircularStd-Medium.woff'; +import BookWoff from 'shared/assets/fonts/CircularStd-Book.woff'; +import IconsSvg from 'shared/assets/icons/jira.svg'; +import IconsTtf from 'shared/assets/icons/jira.ttf'; +import IconsWoff from 'shared/assets/icons/jira.woff'; + +export default createGlobalStyle` + @font-face { + font-family: "CircularStdBlack"; + src: url("${BlackWoff2}") format("woff2"), + url("${BlackWoff}") format("woff"); + font-weight: normal; + font-style: normal; + } + @font-face { + font-family: "CircularStdBold"; + src: url("${BoldWoff2}") format("woff2"), + url("${BoldWoff}") format("woff"); + font-weight: normal; + font-style: normal; + } + @font-face { + font-family: "CircularStdMedium"; + src: url("${MediumWoff2}") format("woff2"), + url("${MediumWoff}") format("woff"); + font-weight: normal; + font-style: normal; + } + @font-face { + font-family: "CircularStdBook"; + src: url("${BookWoff2}") format("woff2"), + url("${BookWoff}") format("woff"); + font-weight: normal; + font-style: normal; + } + @font-face { + font-family: "jira"; + src: + url("${IconsTtf}") format("truetype"), + url("${IconsWoff}") format("woff"), + url("${IconsSvg}#jira") format("svg"); + font-weight: normal; + font-style: normal; + } +`; diff --git a/client/src/components/App/NavbarLeft/Styles.js b/client/src/components/App/NavbarLeft/Styles.js new file mode 100644 index 0000000..1530c47 --- /dev/null +++ b/client/src/components/App/NavbarLeft/Styles.js @@ -0,0 +1,107 @@ +import styled from 'styled-components'; +import { NavLink } from 'react-router-dom'; + +import { font, sizes, color, mixin, zIndexValues } from 'shared/utils/styles'; +import Logo from 'shared/components/Logo'; + +export const NavLeft = styled.aside` + z-index: ${zIndexValues.navLeft}; + position: absolute; + top: 0; + left: 0; + overflow-x: hidden; + height: 100%; + width: ${sizes.appNavBarLeftWidth}px; + background: ${color.primary}; + transition: all 0.1s; + ${mixin.hardwareAccelerate} + &:hover { + width: 260px; + box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.6); + } +`; + +export const LogoLink = styled(NavLink)` + display: block; + position: relative; + left: 0; + margin: 40px 0 40px; + transition: left 0.1s; + &:before { + display: inline-block; + content: ''; + position: absolute; + top: 0; + right: 0; + height: 50px; + width: 20px; + background: ${color.primary}; + } + ${NavLeft}:hover & { + left: 3px; + &:before { + display: none; + } + } +`; + +export const StyledLogo = styled(Logo)` + display: inline-block; + margin-left: 13px; + padding: 10px; + ${mixin.clickable} +`; + +export const IconLink = styled(NavLink)` + display: block; + position: relative; + width: 100%; + height: 60px; + line-height: 60px; + padding-left: 67px; + color: rgba(255, 255, 255, 0.75); + transition: color 0.1s; + ${mixin.clickable} + &:before { + content: ''; + display: none; + position: absolute; + top: 5px; + right: 0; + height: 50px; + width: 5px; + background: #fff; + border-radius: 6px 0 0 6px; + } + &.active, + &:hover { + color: #fff; + } + &.active:before { + display: inline-block; + } + &:hover { + background: rgba(255, 255, 255, 0.1); + } + i { + position: absolute; + left: 27px; + } +`; + +export const LinkText = styled.div` + position: relative; + right: 12px; + visibility: hidden; + opacity: 0; + text-transform: uppercase; + transition: all 0.1s; + transition-property: right, visibility, opacity; + ${font.bold} + ${font.size(12)} + ${NavLeft}:hover & { + right: 0; + visibility: visible; + opacity: 1; + } +`; diff --git a/client/src/components/App/NavbarLeft/index.jsx b/client/src/components/App/NavbarLeft/index.jsx new file mode 100644 index 0000000..da3ef5c --- /dev/null +++ b/client/src/components/App/NavbarLeft/index.jsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import { Icon } from 'shared/components'; +import { NavLeft, LogoLink, StyledLogo, IconLink, LinkText } from './Styles'; + +const NavbarLeft = () => ( + + + + + + + Projects + + + + Subcontractors + + + + Bids + + +); + +export default NavbarLeft; diff --git a/client/src/components/App/NormalizeStyles.js b/client/src/components/App/NormalizeStyles.js new file mode 100644 index 0000000..f054d37 --- /dev/null +++ b/client/src/components/App/NormalizeStyles.js @@ -0,0 +1,152 @@ +import { createGlobalStyle } from 'styled-components'; + +/** DO NOT ALTER THIS FILE. It is a copy of https://necolas.github.io/normalize.css/ */ + +export default createGlobalStyle` + html { + line-height: 1.15; + -webkit-text-size-adjust: 100%; + } + body { + margin: 0; + } + main { + display: block; + } + h1 { + font-size: 2em; + margin: 0.67em 0; + } + hr { + box-sizing: content-box; + height: 0; + overflow: visible; + } + pre { + font-family: monospace, monospace; + font-size: 1em; + } + a { + background-color: transparent; + } + abbr[title] { + border-bottom: none; + text-decoration: underline; + text-decoration: underline dotted; + } + b, + strong { + font-weight: bolder; + } + code, + kbd, + samp { + font-family: monospace, monospace; + font-size: 1em; + } + small { + font-size: 80%; + } + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + img { + border-style: none; + } + button, + input, + optgroup, + select, + textarea { + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0; + } + button, + input { + overflow: visible; + } + button, + select { + text-transform: none; + } + button, + [type="button"], + [type="reset"], + [type="submit"] { + -webkit-appearance: button; + } + button::-moz-focus-inner, + [type="button"]::-moz-focus-inner, + [type="reset"]::-moz-focus-inner, + [type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; + } + button:-moz-focusring, + [type="button"]:-moz-focusring, + [type="reset"]:-moz-focusring, + [type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; + } + fieldset { + padding: 0.35em 0.75em 0.625em; + } + legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal; + } + progress { + vertical-align: baseline; + } + textarea { + overflow: auto; + } + [type="checkbox"], + [type="radio"] { + box-sizing: border-box; + padding: 0; + } + [type="number"]::-webkit-inner-spin-button, + [type="number"]::-webkit-outer-spin-button { + height: auto; + } + [type="search"] { + -webkit-appearance: textfield; + outline-offset: -2px; + } + [type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; + } + details { + display: block; + } + summary { + display: list-item; + } + template { + display: none; + } + [hidden] { + display: none; + } +`; diff --git a/client/src/components/App/Routes.jsx b/client/src/components/App/Routes.jsx new file mode 100644 index 0000000..011a60a --- /dev/null +++ b/client/src/components/App/Routes.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import history from 'browserHistory'; +import { Router, Switch, Route } from 'react-router-dom'; + +import PageNotFound from 'components/PageNotFound'; +import NavbarLeft from './NavbarLeft'; + +import { Main } from './AppStyles'; + +const Routes = () => ( + +
+ + + + +
+
+); + +export default Routes; diff --git a/client/src/components/App/Toast/Styles.js b/client/src/components/App/Toast/Styles.js new file mode 100644 index 0000000..21a15b8 --- /dev/null +++ b/client/src/components/App/Toast/Styles.js @@ -0,0 +1,58 @@ +import styled from 'styled-components'; + +import { color, font, mixin, zIndexValues } from 'shared/utils/styles'; + +export const Container = styled.div` + z-index: ${zIndexValues.modal + 1}; + position: fixed; + right: 30px; + top: 50px; +`; + +export const StyledToast = styled.div` + position: relative; + margin-bottom: 5px; + width: 300px; + padding: 15px 20px; + border-radius: 4px; + color: #fff; + background: ${props => color[props.type]}; + cursor: pointer; + transition: all 0.15s; + ${mixin.clearfix} + ${mixin.hardwareAccelerate} + + &.jira-toast-enter, + &.jira-toast-exit.jira-toast-exit-active { + opacity: 0; + right: -10px; + } + + &.jira-toast-exit, + &.jira-toast-enter.jira-toast-enter-active { + opacity: 1; + right: 0; + } + + i { + position: absolute; + top: 13px; + right: 14px; + font-size: 22px; + cursor: pointer; + color: #fff; + } +`; + +export const Title = styled.div` + padding-right: 22px; + ${font.size(16)} + ${font.medium} +`; + +export const Message = styled.div` + padding: 8px 10px 0 0; + white-space: pre-wrap; + ${font.size(14)} + ${font.medium} +`; diff --git a/client/src/components/App/Toast/index.jsx b/client/src/components/App/Toast/index.jsx new file mode 100644 index 0000000..867fe82 --- /dev/null +++ b/client/src/components/App/Toast/index.jsx @@ -0,0 +1,62 @@ +import React, { Component } from 'react'; +import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import pubsub from 'sweet-pubsub'; +import { uniqueId } from 'lodash'; + +import { Icon } from 'shared/components'; +import { Container, StyledToast, Title, Message } from './Styles'; + +class Toast extends Component { + state = { toasts: [] }; + + componentDidMount() { + pubsub.on('toast', this.addToast); + } + + componentWillUnmount() { + pubsub.off('toast', this.addToast); + } + + addToast = ({ type = 'success', title, message, duration = 5 }) => { + const id = uniqueId('toast-'); + + this.setState(state => ({ + toasts: [...state.toasts, { id, type, title, message }], + })); + + if (duration) { + setTimeout(() => this.removeToast(id), duration * 1000); + } + }; + + removeToast = id => { + this.setState(state => ({ + toasts: state.toasts.filter(toast => toast.id !== id), + })); + }; + + render() { + const { toasts } = this.state; + return ( + + + {toasts.map(toast => ( + + this.removeToast(toast.id)} + > + + {toast.title && {toast.title}} + {toast.message && {toast.message}} + + + ))} + + + ); + } +} + +export default Toast; diff --git a/client/src/components/PageNotFound/Styles.js b/client/src/components/PageNotFound/Styles.js new file mode 100644 index 0000000..1d72f9b --- /dev/null +++ b/client/src/components/PageNotFound/Styles.js @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +import { color, font } from 'shared/utils/styles'; + +export const Wrapper = styled.div` + margin: 50px auto 0; + max-width: 500px; + padding: 50px 50px 60px; + text-align: center; + border-radius: 4px; + background: ${color.backgroundLight}; +`; + +export const Heading = styled.h1` + ${font.size(60)} +`; + +export const Message = styled.p` + color: ${color.textDark}; + padding: 10px 0 30px; + line-height: 1.35; + ${font.size(20)} +`; diff --git a/client/src/components/PageNotFound/index.jsx b/client/src/components/PageNotFound/index.jsx new file mode 100644 index 0000000..266c7ba --- /dev/null +++ b/client/src/components/PageNotFound/index.jsx @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; + +import { + Button, + ConfirmModal, + Avatar, + DatePicker, + Input, + Modal, + Select, + Textarea, + Spinner, +} from 'shared/components'; +import { Wrapper, Heading, Message } from './Styles'; + +const PageNotFound = () => { + const [dateValue, setDateValue] = useState(null); + const [inputValue, setInputValue] = useState(''); + const [isModalOpen, setModalOpen] = useState(false); + const [selectValue, setSelectValue] = useState(''); + const [selectOptions, setSelectOptions] = useState([ + { label: 'one', value: '1' }, + { label: 'two', value: '2' }, + { label: 'three', value: '3' }, + { label: 'four', value: '4' }, + { label: 'five', value: '5' }, + { label: 'six', value: '6' }, + { label: 'seven', value: '7' }, + { label: 'eight', value: '8' }, + { label: 'nine', value: '9' }, + { label: 'ten', value: '10' }, + ]); + console.log('ha'); + return ( + + 404 + We cannot find the page you are looking for. +
+ + } + confirmInput="YAY" + onConfirm={modal => { + console.log('CONFIRMED!'); + modal.close(); + }} + /> + + setInputValue(value)} + /> +