initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react", "@typescript-eslint", "prettier"],
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"airbnb",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
|
||||
"react/prop-types": 0,
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"js": "never",
|
||||
"mjs": "never",
|
||||
"jsx": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
],
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{
|
||||
"devDependencies": [".storybook/**", "src/shared/components/**/*.stories.tsx"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"paths": ["src"],
|
||||
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: "all",
|
||||
singleQuote: true,
|
||||
printWidth: 120,
|
||||
tabWidth: 2
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../src/shared/components/**/*.stories.tsx'],
|
||||
addons: [
|
||||
'@storybook/addon-actions/register',
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-storysource',
|
||||
'@storybook/addon-knobs/register',
|
||||
'@storybook/addon-docs/register',
|
||||
'@storybook/addon-viewport/register',
|
||||
'@storybook/addon-backgrounds/register',
|
||||
],
|
||||
webpackFinal: async config => {
|
||||
config.resolve.modules.push(path.resolve(__dirname, '../src'));
|
||||
return config;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `yarn start`
|
||||
|
||||
Runs the app in the development mode.<br />
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br />
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br />
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br />
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br />
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/react-hooks": "^3.1.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.27",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.12.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.12.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.8",
|
||||
"@storybook/addon-backgrounds": "^5.3.17",
|
||||
"@storybook/addon-docs": "^5.3.17",
|
||||
"@storybook/addon-knobs": "^5.3.17",
|
||||
"@storybook/addon-storysource": "^5.3.17",
|
||||
"@storybook/addon-viewport": "^5.3.17",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9.21",
|
||||
"@types/react-beautiful-dnd": "^12.1.1",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/react-router": "^5.1.4",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"@types/styled-components": "^5.0.0",
|
||||
"apollo-cache-inmemory": "^1.6.5",
|
||||
"apollo-client": "^2.6.8",
|
||||
"apollo-link": "^1.2.13",
|
||||
"apollo-link-error": "^1.1.12",
|
||||
"apollo-link-http": "^1.5.16",
|
||||
"apollo-link-state": "^0.4.2",
|
||||
"apollo-utilities": "^1.3.3",
|
||||
"color": "^3.1.2",
|
||||
"graphql": "^14.6.0",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"history": "^4.10.1",
|
||||
"lodash": "^4.17.15",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.12.0",
|
||||
"react-autosize-textarea": "^7.0.0",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-hook-form": "^5.2.0",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.4.0",
|
||||
"styled-components": "^5.0.1",
|
||||
"typescript": "~3.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"storybook": "start-storybook -p 9009 -s public",
|
||||
"build-storybook": "build-storybook -s public"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^5.3.13",
|
||||
"@storybook/addon-links": "^5.3.13",
|
||||
"@storybook/addons": "^5.3.13",
|
||||
"@storybook/preset-create-react-app": "^1.5.2",
|
||||
"@storybook/react": "^5.3.13",
|
||||
"@typescript-eslint/eslint-plugin": "^2.20.0",
|
||||
"@typescript-eslint/parser": "^2.20.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb": "^18.0.1",
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-react": "^7.18.3",
|
||||
"eslint-plugin-react-hooks": "^1.7.0",
|
||||
"prettier": "^1.19.1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,110 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
|
||||
export default createGlobalStyle`
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
min-width: 768px;
|
||||
}
|
||||
|
||||
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 {
|
||||
color: inherit;
|
||||
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.4285;
|
||||
a {
|
||||
${mixin.link()}
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
line-height: 1.4285;
|
||||
}
|
||||
|
||||
body, select {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
${mixin.placeholderColor(color.textLight)}
|
||||
`;
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Home, Stack } from 'shared/icons';
|
||||
import Navbar, { ActionButton, ButtonContainer, PrimaryLogo } from 'shared/components/Navbar';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const GlobalNavbar = () => {
|
||||
return (
|
||||
<Navbar>
|
||||
<PrimaryLogo />
|
||||
<ButtonContainer>
|
||||
<Link to="/">
|
||||
<ActionButton name="Home">
|
||||
<Home size={28} color="#c2c6dc" />
|
||||
</ActionButton>
|
||||
</Link>
|
||||
<Link to="/projects">
|
||||
<ActionButton name="Projects">
|
||||
<Stack size={28} color="#c2c6dc" />
|
||||
</ActionButton>
|
||||
</Link>
|
||||
</ButtonContainer>
|
||||
</Navbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalNavbar;
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Router, Switch, Route } from 'react-router-dom';
|
||||
import * as H from 'history';
|
||||
|
||||
import Projects from 'Projects';
|
||||
import Project from 'Projects/Project';
|
||||
import Login from 'Auth';
|
||||
|
||||
type RoutesProps = {
|
||||
history: H.History;
|
||||
};
|
||||
|
||||
const Routes = ({ history }: RoutesProps) => (
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route exact path="/projects" component={Projects} />
|
||||
<Route exact path="/projects/:projectId" component={Project} />
|
||||
<Route exact path="/login" component={Login} />
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
|
||||
export default Routes;
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
import TopNavbar from 'shared/components/TopNavbar';
|
||||
import DropdownMenu from 'shared/components/DropdownMenu';
|
||||
|
||||
const GlobalTopNavbar: React.FC = () => {
|
||||
const [menu, setMenu] = useState({
|
||||
top: 0,
|
||||
left: 0,
|
||||
isOpen: false,
|
||||
});
|
||||
const onProfileClick = (bottom: number, right: number) => {
|
||||
setMenu({
|
||||
isOpen: !menu.isOpen,
|
||||
left: right,
|
||||
top: bottom,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<TopNavbar onNotificationClick={() => console.log('beep')} onProfileClick={onProfileClick} />
|
||||
{menu.isOpen && <DropdownMenu left={menu.left} top={menu.top} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalTopNavbar;
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
import NormalizeStyles from './NormalizeStyles';
|
||||
import BaseStyles from './BaseStyles';
|
||||
import Routes from './Routes';
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
const App = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://localhost:3333/auth/refresh_token', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
if (status === 400) {
|
||||
history.replace('/login');
|
||||
} else {
|
||||
const response: RefreshTokenResponse = await x.json();
|
||||
const { accessToken } = response;
|
||||
setAccessToken(accessToken);
|
||||
}
|
||||
// }
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div>loading...</div>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<Routes history={history} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
`;
|
||||
|
||||
export const LoginWrapper = styled.div`
|
||||
width: 60%;
|
||||
`;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
import { setAccessToken } from 'shared/utils/accessToken';
|
||||
|
||||
import Login from 'shared/components/Login';
|
||||
import { Container, LoginWrapper } from './Styles';
|
||||
|
||||
const Auth = () => {
|
||||
const [invalidLoginAttempt, setInvalidLoginAttempt] = useState(0);
|
||||
const history = useHistory();
|
||||
const login = (
|
||||
data: LoginFormData,
|
||||
setComplete: (val: boolean) => void,
|
||||
setError: (field: string, eType: string, message: string) => void,
|
||||
) => {
|
||||
fetch('http://localhost:3333/auth/login', {
|
||||
credentials: 'include',
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
}),
|
||||
}).then(async x => {
|
||||
if (x.status === 401) {
|
||||
setInvalidLoginAttempt(invalidLoginAttempt + 1);
|
||||
setError('username', 'invalid', 'Invalid username');
|
||||
setError('password', 'invalid', 'Invalid password');
|
||||
setComplete(true);
|
||||
} else {
|
||||
const response = await x.json();
|
||||
const { accessToken } = response;
|
||||
setAccessToken(accessToken);
|
||||
setComplete(true);
|
||||
history.push('/');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://localhost:3333/auth/refresh_token', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(async x => {
|
||||
const { status } = x;
|
||||
if (status === 200) {
|
||||
history.replace('/projects');
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LoginWrapper>
|
||||
<Login onSubmit={login} />
|
||||
</LoginWrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Auth;
|
||||
@@ -0,0 +1,322 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { useQuery, useMutation } from '@apollo/react-hooks';
|
||||
import gql from 'graphql-tag';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import Navbar from 'App/Navbar';
|
||||
import TopNavbar from 'App/TopNavbar';
|
||||
import Lists from 'shared/components/Lists';
|
||||
import QuickCardEditor from 'shared/components/QuickCardEditor';
|
||||
|
||||
interface ColumnState {
|
||||
[key: string]: TaskGroup;
|
||||
}
|
||||
|
||||
interface TaskState {
|
||||
[key: string]: RemoteTask;
|
||||
}
|
||||
|
||||
interface State {
|
||||
columns: ColumnState;
|
||||
tasks: TaskState;
|
||||
}
|
||||
|
||||
interface QuickCardEditorState {
|
||||
isOpen: boolean;
|
||||
left: number;
|
||||
top: number;
|
||||
task?: RemoteTask;
|
||||
}
|
||||
|
||||
const MainContent = styled.div`
|
||||
padding: 0 0 50px 100px;
|
||||
height: 100%;
|
||||
background: #262c49;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 16px;
|
||||
background-color: red;
|
||||
`;
|
||||
|
||||
const Title = styled.span`
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
`;
|
||||
|
||||
interface ProjectData {
|
||||
findProject: Project;
|
||||
}
|
||||
|
||||
interface UpdateTaskLocationData {
|
||||
updateTaskLocation: Task;
|
||||
}
|
||||
|
||||
interface UpdateTaskLocationVars {
|
||||
taskID: string;
|
||||
taskGroupID: string;
|
||||
position: number;
|
||||
}
|
||||
|
||||
interface ProjectVars {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
interface CreateTaskVars {
|
||||
taskGroupID: string;
|
||||
name: string;
|
||||
position: number;
|
||||
}
|
||||
|
||||
interface CreateTaskData {
|
||||
createTask: RemoteTask;
|
||||
}
|
||||
|
||||
interface ProjectParams {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
interface DeleteTaskData {
|
||||
deleteTask: { taskID: string };
|
||||
}
|
||||
|
||||
interface DeleteTaskVars {
|
||||
taskID: string;
|
||||
}
|
||||
|
||||
interface UpdateTaskNameData {
|
||||
updateTaskName: RemoteTask;
|
||||
}
|
||||
|
||||
interface UpdateTaskNameVars {
|
||||
taskID: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const UPDATE_TASK_NAME = gql`
|
||||
mutation updateTaskName($taskID: String!, $name: String!) {
|
||||
updateTaskName(input: { taskID: $taskID, name: $name }) {
|
||||
taskID
|
||||
name
|
||||
position
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const GET_PROJECT = gql`
|
||||
query getProject($projectId: String!) {
|
||||
findProject(input: { projectId: $projectId }) {
|
||||
name
|
||||
taskGroups {
|
||||
taskGroupID
|
||||
name
|
||||
position
|
||||
tasks {
|
||||
taskID
|
||||
name
|
||||
position
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CREATE_TASK = gql`
|
||||
mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
|
||||
createTask(input: { taskGroupID: $taskGroupID, name: $name, position: $position }) {
|
||||
taskID
|
||||
taskGroupID
|
||||
name
|
||||
position
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DELETE_TASK = gql`
|
||||
mutation deleteTask($taskID: String!) {
|
||||
deleteTask(input: { taskID: $taskID }) {
|
||||
taskID
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const UPDATE_TASK_LOCATION = gql`
|
||||
mutation updateTaskLocation($taskID: String!, $taskGroupID: String!, $position: Float!) {
|
||||
updateTaskLocation(input: { taskID: $taskID, taskGroupID: $taskGroupID, position: $position }) {
|
||||
taskID
|
||||
createdAt
|
||||
name
|
||||
position
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const initialState: State = { tasks: {}, columns: {} };
|
||||
const initialQuickCardEditorState: QuickCardEditorState = { isOpen: false, top: 0, left: 0 };
|
||||
|
||||
const Project = () => {
|
||||
const { projectId } = useParams<ProjectParams>();
|
||||
const [listsData, setListsData] = useState(initialState);
|
||||
const [quickCardEditor, setQuickCardEditor] = useState(initialQuickCardEditorState);
|
||||
const [updateTaskLocation, updateTaskLocationData] = useMutation<UpdateTaskLocationData, UpdateTaskLocationVars>(
|
||||
UPDATE_TASK_LOCATION,
|
||||
);
|
||||
const [createTask, createTaskData] = useMutation<CreateTaskData, CreateTaskVars>(CREATE_TASK, {
|
||||
onCompleted: newTaskData => {
|
||||
const newListsData = {
|
||||
...listsData,
|
||||
tasks: {
|
||||
...listsData.tasks,
|
||||
[newTaskData.createTask.taskID]: {
|
||||
taskGroupID: newTaskData.createTask.taskGroupID,
|
||||
taskID: newTaskData.createTask.taskID,
|
||||
name: newTaskData.createTask.name,
|
||||
position: newTaskData.createTask.position,
|
||||
labels: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
setListsData(newListsData);
|
||||
},
|
||||
});
|
||||
const [deleteTask, deleteTaskData] = useMutation<DeleteTaskData, DeleteTaskVars>(DELETE_TASK, {
|
||||
onCompleted: deletedTask => {
|
||||
const { [deletedTask.deleteTask.taskID]: removedTask, ...remainingTasks } = listsData.tasks;
|
||||
const newListsData = {
|
||||
...listsData,
|
||||
tasks: remainingTasks,
|
||||
};
|
||||
setListsData(newListsData);
|
||||
},
|
||||
});
|
||||
const [updateTaskName, updateTaskNameData] = useMutation<UpdateTaskNameData, UpdateTaskNameVars>(UPDATE_TASK_NAME, {
|
||||
onCompleted: newTaskData => {
|
||||
const newListsData = {
|
||||
...listsData,
|
||||
tasks: {
|
||||
...listsData.tasks,
|
||||
[newTaskData.updateTaskName.taskID]: {
|
||||
...listsData.tasks[newTaskData.updateTaskName.taskID],
|
||||
name: newTaskData.updateTaskName.name,
|
||||
},
|
||||
},
|
||||
};
|
||||
setListsData(newListsData);
|
||||
},
|
||||
});
|
||||
const { loading, data } = useQuery<ProjectData, ProjectVars>(GET_PROJECT, {
|
||||
variables: { projectId },
|
||||
onCompleted: newData => {
|
||||
let newListsData: State = { tasks: {}, columns: {} };
|
||||
newData.findProject.taskGroups.forEach((taskGroup: TaskGroup) => {
|
||||
newListsData.columns[taskGroup.taskGroupID] = {
|
||||
taskGroupID: taskGroup.taskGroupID,
|
||||
name: taskGroup.name,
|
||||
position: taskGroup.position,
|
||||
tasks: [],
|
||||
};
|
||||
taskGroup.tasks.forEach((task: RemoteTask) => {
|
||||
newListsData.tasks[task.taskID] = {
|
||||
taskID: task.taskID,
|
||||
taskGroupID: taskGroup.taskGroupID,
|
||||
name: task.name,
|
||||
position: task.position,
|
||||
labels: [],
|
||||
};
|
||||
});
|
||||
});
|
||||
setListsData(newListsData);
|
||||
},
|
||||
});
|
||||
const onCardDrop = (droppedTask: any) => {
|
||||
updateTaskLocation({
|
||||
variables: { taskID: droppedTask.taskID, taskGroupID: droppedTask.taskGroupID, position: droppedTask.position },
|
||||
});
|
||||
const newState = {
|
||||
...listsData,
|
||||
tasks: {
|
||||
...listsData.tasks,
|
||||
[droppedTask.taskID]: droppedTask,
|
||||
},
|
||||
};
|
||||
setListsData(newState);
|
||||
};
|
||||
const onListDrop = (droppedColumn: any) => {
|
||||
const newState = {
|
||||
...listsData,
|
||||
columns: {
|
||||
...listsData.columns,
|
||||
[droppedColumn.taskGroupID]: droppedColumn,
|
||||
},
|
||||
};
|
||||
setListsData(newState);
|
||||
};
|
||||
const onCardCreate = (taskGroupID: string, name: string) => {
|
||||
const taskGroupTasks = Object.values(listsData.tasks).filter(
|
||||
(task: RemoteTask) => task.taskGroupID === taskGroupID,
|
||||
);
|
||||
var position = 65535;
|
||||
console.log(taskGroupID);
|
||||
console.log(taskGroupTasks);
|
||||
if (taskGroupTasks.length !== 0) {
|
||||
const [lastTask] = taskGroupTasks.sort((a: any, b: any) => a.position - b.position).slice(-1);
|
||||
console.log(`last tasks position ${lastTask.position}`);
|
||||
position = Math.ceil(lastTask.position) * 2 + 1;
|
||||
}
|
||||
|
||||
createTask({ variables: { taskGroupID: taskGroupID, name: name, position: position } });
|
||||
};
|
||||
const onQuickEditorOpen = (e: ContextMenuEvent) => {
|
||||
const task = Object.values(listsData.tasks).find(task => task.taskID === e.cardId);
|
||||
setQuickCardEditor({
|
||||
top: e.top,
|
||||
left: e.left,
|
||||
isOpen: true,
|
||||
task,
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Wrapper>Loading</Wrapper>;
|
||||
}
|
||||
if (data) {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<MainContent>
|
||||
<TopNavbar />
|
||||
<Title>{data.findProject.name}</Title>
|
||||
<Lists
|
||||
onQuickEditorOpen={onQuickEditorOpen}
|
||||
onCardCreate={onCardCreate}
|
||||
{...listsData}
|
||||
onCardDrop={onCardDrop}
|
||||
onListDrop={onListDrop}
|
||||
/>
|
||||
</MainContent>
|
||||
{quickCardEditor.isOpen && (
|
||||
<QuickCardEditor
|
||||
isOpen={true}
|
||||
listId={quickCardEditor.task ? quickCardEditor.task.taskGroupID : ''}
|
||||
cardId={quickCardEditor.task ? quickCardEditor.task.taskID : ''}
|
||||
cardTitle={quickCardEditor.task ? quickCardEditor.task.name : ''}
|
||||
onCloseEditor={() => setQuickCardEditor(initialQuickCardEditorState)}
|
||||
onEditCard={(listId: string, cardId: string, cardName: string) =>
|
||||
updateTaskName({ variables: { taskID: cardId, name: cardName } })
|
||||
}
|
||||
onOpenPopup={() => console.log()}
|
||||
onArchiveCard={(listId: string, cardId: string) => deleteTask({ variables: { taskID: cardId } })}
|
||||
labels={[]}
|
||||
top={quickCardEditor.top}
|
||||
left={quickCardEditor.left}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Wrapper>Error</Wrapper>;
|
||||
};
|
||||
|
||||
export default Project;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import TopNavbar from 'App/TopNavbar';
|
||||
import ProjectGridItem from 'shared/components/ProjectGridItem';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Navbar from 'App/Navbar';
|
||||
|
||||
const MainContent = styled.div`
|
||||
padding: 0 0 50px 80px;
|
||||
height: 100%;
|
||||
background: #262c49;
|
||||
`;
|
||||
|
||||
const ProjectGrid = styled.div`
|
||||
width: 60%;
|
||||
margin: 25px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
const Wrapper = styled.div`
|
||||
font-size: 16px;
|
||||
background-color: red;
|
||||
`;
|
||||
|
||||
interface ProjectData {
|
||||
name: string;
|
||||
organizations: Organization[];
|
||||
}
|
||||
|
||||
const GET_PROJECTS = gql`
|
||||
query getProjects {
|
||||
organizations {
|
||||
name
|
||||
teams {
|
||||
name
|
||||
projects {
|
||||
name
|
||||
projectID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Projects = () => {
|
||||
const { loading, data } = useQuery<ProjectData>(GET_PROJECTS);
|
||||
console.log(loading, data);
|
||||
if (loading) {
|
||||
return <Wrapper>Loading</Wrapper>;
|
||||
}
|
||||
if (data) {
|
||||
const { teams } = data.organizations[0];
|
||||
const projects: Project[] = [];
|
||||
teams.forEach(team =>
|
||||
team.projects.forEach(project => {
|
||||
projects.push({
|
||||
taskGroups: [],
|
||||
projectID: project.projectID,
|
||||
teamTitle: team.name,
|
||||
name: project.name,
|
||||
color: '#aa62e3',
|
||||
});
|
||||
}),
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<MainContent>
|
||||
<TopNavbar />
|
||||
<ProjectGrid>
|
||||
{projects.map(project => (
|
||||
<Link to={`/projects/${project.projectID}/`}>
|
||||
<ProjectGridItem project={project} />
|
||||
</Link>
|
||||
))}
|
||||
</ProjectGrid>
|
||||
</MainContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Wrapper>Error</Wrapper>;
|
||||
};
|
||||
|
||||
export default Projects;
|
||||
Vendored
+65
@@ -0,0 +1,65 @@
|
||||
type ContextMenuEvent = {
|
||||
left: number;
|
||||
top: number;
|
||||
cardId: string;
|
||||
listId: string;
|
||||
};
|
||||
|
||||
interface RemoteTask {
|
||||
taskID: string;
|
||||
taskGroupID: string;
|
||||
name: string;
|
||||
position: number;
|
||||
labels: Label[];
|
||||
}
|
||||
type TaskGroup = {
|
||||
taskGroupID: string;
|
||||
name: string;
|
||||
position: number;
|
||||
tasks: RemoteTask[];
|
||||
};
|
||||
type Project = {
|
||||
projectID: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
teamTitle?: string;
|
||||
taskGroups: TaskGroup[];
|
||||
};
|
||||
|
||||
interface Organization {
|
||||
name: string;
|
||||
teams: Team[];
|
||||
}
|
||||
|
||||
interface Team {
|
||||
name: string;
|
||||
projects: Project[];
|
||||
}
|
||||
type Label = {
|
||||
labelId: string;
|
||||
name: string;
|
||||
color: string;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
type Task = {
|
||||
title: string;
|
||||
position: number;
|
||||
};
|
||||
|
||||
type RefreshTokenResponse = {
|
||||
accessToken: string;
|
||||
};
|
||||
|
||||
type LoginFormData = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
type LoginProps = {
|
||||
onSubmit: (
|
||||
data: LoginFormData,
|
||||
setComplete: (val: boolean) => void,
|
||||
setError: (field: string, eType: string, message: string) => void,
|
||||
) => void;
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { ApolloProvider } from '@apollo/react-hooks';
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import { InMemoryCache } from 'apollo-cache-inmemory';
|
||||
import { HttpLink } from 'apollo-link-http';
|
||||
import { onError } from 'apollo-link-error';
|
||||
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
|
||||
|
||||
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
|
||||
|
||||
import App from './App';
|
||||
|
||||
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
|
||||
|
||||
let isRefreshing = false;
|
||||
let pendingRequests: any = [];
|
||||
|
||||
const resolvePendingRequests = () => {
|
||||
pendingRequests.map((callback: any) => callback());
|
||||
pendingRequests = [];
|
||||
};
|
||||
|
||||
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
|
||||
if (graphQLErrors) {
|
||||
for (const err of graphQLErrors) {
|
||||
switch (err!.extensions!.code) {
|
||||
case 'UNAUTHENTICATED':
|
||||
// error code is set to UNAUTHENTICATED
|
||||
// when AuthenticationError thrown in resolver
|
||||
let forward$;
|
||||
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
forward$ = fromPromise(
|
||||
getNewToken()
|
||||
.then((response: any) => {
|
||||
// Store the new tokens for your auth link
|
||||
setAccessToken(response.accessToken);
|
||||
resolvePendingRequests();
|
||||
return response.accessToken;
|
||||
})
|
||||
.catch((error: any) => {
|
||||
pendingRequests = [];
|
||||
// TODO
|
||||
// Handle token refresh errors e.g clear stored tokens, redirect to login, ...
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
isRefreshing = false;
|
||||
}),
|
||||
).filter(value => Boolean(value));
|
||||
} else {
|
||||
// Will only emit once the Promise is resolved
|
||||
forward$ = fromPromise(
|
||||
new Promise(resolve => {
|
||||
pendingRequests.push(() => resolve());
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return forward$.flatMap(() => forward(operation));
|
||||
default:
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
if (networkError) {
|
||||
console.log(`[Network error]: ${networkError}`);
|
||||
// if you would also like to retry automatically on
|
||||
// network errors, we recommend that you use
|
||||
// apollo-link-retry
|
||||
}
|
||||
});
|
||||
|
||||
const requestLink = new ApolloLink(
|
||||
(operation, forward) =>
|
||||
new Observable((observer: any) => {
|
||||
let handle: any;
|
||||
Promise.resolve(operation)
|
||||
.then((operation: any) => {
|
||||
const accessToken = getAccessToken();
|
||||
if (accessToken) {
|
||||
operation.setContext({
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
handle = forward(operation).subscribe({
|
||||
next: observer.next.bind(observer),
|
||||
error: observer.error.bind(observer),
|
||||
complete: observer.complete.bind(observer),
|
||||
});
|
||||
})
|
||||
.catch(observer.error.bind(observer));
|
||||
|
||||
return () => {
|
||||
if (handle) handle.unsubscribe();
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const client = new ApolloClient({
|
||||
link: ApolloLink.from([
|
||||
onError(({ graphQLErrors, networkError }) => {
|
||||
if (graphQLErrors)
|
||||
graphQLErrors.forEach(({ message, locations, path }) =>
|
||||
console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`),
|
||||
);
|
||||
if (networkError) console.log(`[Network error]: ${networkError}`);
|
||||
}),
|
||||
errorLink,
|
||||
requestLink,
|
||||
new HttpLink({
|
||||
uri: 'http://localhost:3333/graphql',
|
||||
credentials: 'same-origin',
|
||||
}),
|
||||
]),
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<ApolloProvider client={client}>
|
||||
<App />
|
||||
</ApolloProvider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
@@ -0,0 +1,116 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import LabelColors from 'shared/constants/labelColors';
|
||||
import Card from './index';
|
||||
|
||||
export default {
|
||||
component: Card,
|
||||
title: 'Card',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'gray', value: '#f8f8f8', default: true },
|
||||
{ name: 'white', value: '#ffffff' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const labelData = [
|
||||
{
|
||||
labelId: 'development',
|
||||
name: 'Development',
|
||||
color: LabelColors.BLUE,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
labelId: 'general',
|
||||
name: 'General',
|
||||
color: LabelColors.PINK,
|
||||
active: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const Default = () => {
|
||||
const $ref = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Card
|
||||
cardId="1"
|
||||
listId="1"
|
||||
description=""
|
||||
ref={$ref}
|
||||
title="Hello, world"
|
||||
onClick={action('on click')}
|
||||
onContextMenu={action('on context click')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Labels = () => {
|
||||
const $ref = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Card
|
||||
cardId="1"
|
||||
listId="1"
|
||||
description=""
|
||||
ref={$ref}
|
||||
title="Hello, world"
|
||||
labels={labelData}
|
||||
onClick={action('on click')}
|
||||
onContextMenu={action('on context click')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Badges = () => {
|
||||
const $ref = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Card
|
||||
cardId="1"
|
||||
listId="1"
|
||||
description="hello!"
|
||||
ref={$ref}
|
||||
title="Hello, world"
|
||||
onClick={action('on click')}
|
||||
onContextMenu={action('on context click')}
|
||||
watched
|
||||
checklists={{ complete: 1, total: 4 }}
|
||||
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PastDue = () => {
|
||||
const $ref = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Card
|
||||
cardId="1"
|
||||
listId="1"
|
||||
description="hello!"
|
||||
ref={$ref}
|
||||
title="Hello, world"
|
||||
onClick={action('on click')}
|
||||
onContextMenu={action('on context click')}
|
||||
watched
|
||||
checklists={{ complete: 1, total: 4 }}
|
||||
dueDate={{ isPastDue: true, formattedDate: 'Oct 26, 2020' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Everything = () => {
|
||||
const $ref = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<Card
|
||||
cardId="1"
|
||||
listId="1"
|
||||
description="hello!"
|
||||
ref={$ref}
|
||||
title="Hello, world"
|
||||
onClick={action('on click')}
|
||||
onContextMenu={action('on context click')}
|
||||
watched
|
||||
labels={labelData}
|
||||
checklists={{ complete: 1, total: 4 }}
|
||||
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
|
||||
export const ClockIcon = styled(FontAwesomeIcon)``;
|
||||
|
||||
export const ListCardBadges = styled.div`
|
||||
float: left;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
margin-left: -2px;
|
||||
`;
|
||||
|
||||
export const ListCardBadge = styled.div`
|
||||
color: #5e6c84;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 6px 4px 0;
|
||||
max-width: 100%;
|
||||
min-height: 20px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 2px;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: top;
|
||||
`;
|
||||
|
||||
export const DescriptionBadge = styled(ListCardBadge)`
|
||||
padding-right: 6px;
|
||||
`;
|
||||
|
||||
export const DueDateCardBadge = styled(ListCardBadge)<{ isPastDue: boolean }>`
|
||||
${props =>
|
||||
props.isPastDue &&
|
||||
css`
|
||||
padding-left: 4px;
|
||||
background-color: #ec9488;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const ListCardBadgeText = styled.span`
|
||||
font-size: 12px;
|
||||
padding: 0 4px 0 6px;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const ListCardContainer = styled.div<{ isActive: boolean }>`
|
||||
max-width: 256px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
${mixin.boxShadowCard}
|
||||
cursor: pointer !important;
|
||||
position: relative;
|
||||
|
||||
background-color: ${props => (props.isActive ? mixin.darken('#262c49', 0.1) : mixin.lighten('#262c49', 0.05))};
|
||||
`;
|
||||
|
||||
export const ListCardInnerContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const ListCardDetails = styled.div`
|
||||
overflow: hidden;
|
||||
padding: 6px 8px 2px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
export const ListCardLabels = styled.div`
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ListCardLabel = styled.span`
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
padding: 0 8px;
|
||||
max-width: 198px;
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin: 0 4px 4px 0;
|
||||
width: auto;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
display: block;
|
||||
position: relative;
|
||||
background-color: ${props => props.color};
|
||||
`;
|
||||
|
||||
export const ListCardOperation = styled.span`
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
background-color: ${props => mixin.darken('#262c49', 0.15)};
|
||||
background-clip: padding-box;
|
||||
background-origin: padding-box;
|
||||
border-radius: 3px;
|
||||
opacity: 0.8;
|
||||
padding: 6px;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
export const CardTitle = styled.span`
|
||||
font-family: 'Droid Sans';
|
||||
clear: both;
|
||||
display: block;
|
||||
margin: 0 0 4px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
word-wrap: break-word;
|
||||
color: #c2c6dc;
|
||||
`;
|
||||
@@ -0,0 +1,144 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { DraggableProvidedDraggableProps } from 'react-beautiful-dnd';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPencilAlt, faList } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faClock, faCheckSquare, faEye } from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
DescriptionBadge,
|
||||
DueDateCardBadge,
|
||||
ListCardBadges,
|
||||
ListCardBadge,
|
||||
ListCardBadgeText,
|
||||
ListCardContainer,
|
||||
ListCardInnerContainer,
|
||||
ListCardDetails,
|
||||
ClockIcon,
|
||||
ListCardLabels,
|
||||
ListCardLabel,
|
||||
ListCardOperation,
|
||||
CardTitle,
|
||||
} from './Styles';
|
||||
|
||||
type DueDate = {
|
||||
isPastDue: boolean;
|
||||
formattedDate: string;
|
||||
};
|
||||
|
||||
type Checklist = {
|
||||
complete: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
cardId: string;
|
||||
listId: string;
|
||||
onContextMenu: (e: ContextMenuEvent) => void;
|
||||
onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
dueDate?: DueDate;
|
||||
checklists?: Checklist;
|
||||
watched?: boolean;
|
||||
labels?: Label[];
|
||||
wrapperProps?: any;
|
||||
};
|
||||
|
||||
const Card = React.forwardRef(
|
||||
(
|
||||
{
|
||||
wrapperProps,
|
||||
onContextMenu,
|
||||
cardId,
|
||||
listId,
|
||||
onClick,
|
||||
labels,
|
||||
title,
|
||||
dueDate,
|
||||
description,
|
||||
checklists,
|
||||
watched,
|
||||
}: Props,
|
||||
$cardRef: any,
|
||||
) => {
|
||||
const [isActive, setActive] = useState(false);
|
||||
const $innerCardRef: any = useRef(null);
|
||||
const onOpenComposer = () => {
|
||||
if (typeof $innerCardRef.current !== 'undefined') {
|
||||
const pos = $innerCardRef.current.getBoundingClientRect();
|
||||
onContextMenu({
|
||||
top: pos.top,
|
||||
left: pos.left,
|
||||
listId,
|
||||
cardId,
|
||||
});
|
||||
}
|
||||
};
|
||||
const onTaskContext = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenComposer();
|
||||
};
|
||||
const onOperationClick = (e: React.MouseEvent<HTMLOrSVGElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onOpenComposer();
|
||||
};
|
||||
return (
|
||||
<ListCardContainer
|
||||
onMouseEnter={() => setActive(true)}
|
||||
onMouseLeave={() => setActive(false)}
|
||||
ref={$cardRef}
|
||||
onClick={onClick}
|
||||
onContextMenu={onTaskContext}
|
||||
isActive={isActive}
|
||||
{...wrapperProps}
|
||||
>
|
||||
<ListCardInnerContainer ref={$innerCardRef}>
|
||||
<ListCardOperation>
|
||||
<FontAwesomeIcon onClick={onOperationClick} color="#c2c6dc" size="xs" icon={faPencilAlt} />
|
||||
</ListCardOperation>
|
||||
<ListCardDetails>
|
||||
<ListCardLabels>
|
||||
{labels &&
|
||||
labels.map(label => (
|
||||
<ListCardLabel color={label.color} key={label.name}>
|
||||
{label.name}
|
||||
</ListCardLabel>
|
||||
))}
|
||||
</ListCardLabels>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<ListCardBadges>
|
||||
{watched && (
|
||||
<ListCardBadge>
|
||||
<FontAwesomeIcon color="#6b778c" icon={faEye} size="xs" />
|
||||
</ListCardBadge>
|
||||
)}
|
||||
{dueDate && (
|
||||
<DueDateCardBadge isPastDue={dueDate.isPastDue}>
|
||||
<ClockIcon color={dueDate.isPastDue ? '#fff' : '#6b778c'} icon={faClock} size="xs" />
|
||||
<ListCardBadgeText>{dueDate.formattedDate}</ListCardBadgeText>
|
||||
</DueDateCardBadge>
|
||||
)}
|
||||
{description && (
|
||||
<DescriptionBadge>
|
||||
<FontAwesomeIcon color="#6b778c" icon={faList} size="xs" />
|
||||
</DescriptionBadge>
|
||||
)}
|
||||
{checklists && (
|
||||
<ListCardBadge>
|
||||
<FontAwesomeIcon color="#6b778c" icon={faCheckSquare} size="xs" />
|
||||
<ListCardBadgeText>{`${checklists.complete}/${checklists.total}`}</ListCardBadgeText>
|
||||
</ListCardBadge>
|
||||
)}
|
||||
</ListCardBadges>
|
||||
</ListCardDetails>
|
||||
</ListCardInnerContainer>
|
||||
</ListCardContainer>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
export default Card;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import CardComposer from './index';
|
||||
|
||||
export default {
|
||||
component: CardComposer,
|
||||
title: 'CardComposer',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'gray', value: '#f8f8f8', default: true },
|
||||
{ name: 'white', value: '#ffffff' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return <CardComposer isOpen onClose={action('on close')} onCreateCard={action('on create card')} />;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import styled from 'styled-components';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
|
||||
export const CancelIcon = styled(FontAwesomeIcon)`
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
font-size: 1.25em;
|
||||
padding-left: 5px;
|
||||
`;
|
||||
export const CardComposerWrapper = styled.div<{ isOpen: boolean }>`
|
||||
padding-bottom: 8px;
|
||||
display: ${props => (props.isOpen ? 'flex' : 'none')};
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const ListCard = styled.div`
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
${mixin.boxShadowCard}
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
max-width: 300px;
|
||||
min-height: 20px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
z-index: 0;
|
||||
`;
|
||||
|
||||
export const ListCardDetails = styled.div`
|
||||
overflow: hidden;
|
||||
padding: 6px 8px 2px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
export const ListCardLabels = styled.div``;
|
||||
|
||||
export const ListCardEditor = styled(TextareaAutosize)`
|
||||
font-family: 'Droid Sans';
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
resize: none;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
margin-bottom: 4px;
|
||||
max-height: 162px;
|
||||
min-height: 54px;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ComposerControls = styled.div``;
|
||||
|
||||
export const ComposerControlsSaveSection = styled.div`
|
||||
display: flex;
|
||||
float: left;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
export const ComposerControlsActionsSection = styled.div`
|
||||
float: right;
|
||||
`;
|
||||
|
||||
export const AddCardButton = styled.button`
|
||||
background-color: #5aac44;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
margin-right: 4px;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
`;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
|
||||
import {
|
||||
CardComposerWrapper,
|
||||
CancelIcon,
|
||||
AddCardButton,
|
||||
ListCard,
|
||||
ListCardDetails,
|
||||
ListCardEditor,
|
||||
ComposerControls,
|
||||
ComposerControlsSaveSection,
|
||||
ComposerControlsActionsSection,
|
||||
} from './Styles';
|
||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onCreateCard: (cardName: string) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const CardComposer = ({ isOpen, onCreateCard, onClose }: Props) => {
|
||||
const [cardName, setCardName] = useState('');
|
||||
const $cardEditor: any = useRef(null);
|
||||
const onClick = () => {
|
||||
onCreateCard(cardName);
|
||||
};
|
||||
const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onCreateCard(cardName);
|
||||
}
|
||||
};
|
||||
const onBlur = () => {
|
||||
if (cardName === '') {
|
||||
onClose();
|
||||
} else {
|
||||
onCreateCard(cardName);
|
||||
}
|
||||
};
|
||||
useOnEscapeKeyDown(isOpen, onClose);
|
||||
useOnOutsideClick($cardEditor, true, () => onClose(), null);
|
||||
useEffect(() => {
|
||||
$cardEditor.current.focus();
|
||||
}, []);
|
||||
return (
|
||||
<CardComposerWrapper isOpen={isOpen}>
|
||||
<ListCard>
|
||||
<ListCardDetails>
|
||||
<ListCardEditor
|
||||
onKeyDown={onKeyDown}
|
||||
ref={$cardEditor}
|
||||
onChange={e => {
|
||||
setCardName(e.currentTarget.value);
|
||||
}}
|
||||
value={cardName}
|
||||
placeholder="Enter a title for this card..."
|
||||
/>
|
||||
</ListCardDetails>
|
||||
</ListCard>
|
||||
<ComposerControls>
|
||||
<ComposerControlsSaveSection>
|
||||
<AddCardButton onClick={onClick}>Add Card</AddCardButton>
|
||||
<CancelIcon onClick={onClose} icon={faTimes} color="#42526e" />
|
||||
</ComposerControlsSaveSection>
|
||||
<ComposerControlsActionsSection />
|
||||
</ComposerControls>
|
||||
</CardComposerWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
CardComposer.propTypes = {
|
||||
isOpen: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onCreateCard: PropTypes.func.isRequired,
|
||||
};
|
||||
CardComposer.defaultProps = {
|
||||
isOpen: true,
|
||||
};
|
||||
|
||||
export default CardComposer;
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { createRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import DropdownMenu from './index';
|
||||
|
||||
export default {
|
||||
component: DropdownMenu,
|
||||
title: 'DropdownMenu',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff' },
|
||||
{ name: 'gray', value: '#f8f8f8' },
|
||||
{ name: 'darkBlue', value: '#262c49', default: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
const Button = styled.div`
|
||||
font-size: 18px;
|
||||
padding: 15px 20px;
|
||||
color: #fff;
|
||||
background: #000;
|
||||
`;
|
||||
|
||||
export const Default = () => {
|
||||
const [menu, setMenu] = useState({
|
||||
top: 0,
|
||||
left: 0,
|
||||
isOpen: false,
|
||||
});
|
||||
const $buttonRef: any = createRef();
|
||||
const onClick = () => {
|
||||
console.log($buttonRef.current.getBoundingClientRect());
|
||||
setMenu({
|
||||
isOpen: !menu.isOpen,
|
||||
left: $buttonRef.current.getBoundingClientRect().right,
|
||||
top: $buttonRef.current.getBoundingClientRect().bottom,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Button onClick={onClick} ref={$buttonRef}>
|
||||
Click me
|
||||
</Button>
|
||||
</Container>
|
||||
{menu.isOpen && <DropdownMenu left={menu.left} top={menu.top} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
export const Container = styled.div<{ left: number; top: number }>`
|
||||
position: absolute;
|
||||
left: ${props => props.left}px;
|
||||
top: ${props => props.top}px;
|
||||
padding-top: 10px;
|
||||
position: absolute;
|
||||
height: auto;
|
||||
width: auto;
|
||||
transform: translate(-100%);
|
||||
transition: opacity 0.25s, transform 0.25s, width 0.3s ease;
|
||||
z-index: 40000;
|
||||
`;
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 5px;
|
||||
padding-top: 8px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
margin: 0;
|
||||
|
||||
color: #c2c6dc;
|
||||
background: #262c49;
|
||||
border-color: #414561;
|
||||
`;
|
||||
|
||||
export const WrapperDiamond = styled.div`
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
display: block;
|
||||
transform: rotate(45deg) translate(-7px);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
|
||||
background: #262c49;
|
||||
border-color: #414561;
|
||||
`;
|
||||
|
||||
export const ActionsList = styled.ul`
|
||||
min-width: 9rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export const ActionItem = styled.li`
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: 0.5rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
&:hover {
|
||||
background: rgb(115, 103, 240);
|
||||
}
|
||||
`;
|
||||
|
||||
export const ActionTitle = styled.span`
|
||||
margin-left: 0.5rem;
|
||||
`;
|
||||
|
||||
export const Separator = styled.div`
|
||||
height: 1px;
|
||||
border-top: 1px solid #414561;
|
||||
margin: 0.25rem !important;
|
||||
`;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Exit, User } from 'shared/icons';
|
||||
import { Separator, Container, WrapperDiamond, Wrapper, ActionsList, ActionItem, ActionTitle } from './Styles';
|
||||
|
||||
type DropdownMenuProps = {
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
|
||||
const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top }) => {
|
||||
return (
|
||||
<Container left={left} top={top}>
|
||||
<Wrapper>
|
||||
<ActionItem>
|
||||
<User size={16} color="#c2c6dc" />
|
||||
<ActionTitle>Profile</ActionTitle>
|
||||
</ActionItem>
|
||||
<Separator />
|
||||
<ActionsList>
|
||||
<ActionItem>
|
||||
<Exit size={16} color="#c2c6dc" />
|
||||
<ActionTitle>Logout</ActionTitle>
|
||||
</ActionItem>
|
||||
</ActionsList>
|
||||
</Wrapper>
|
||||
<WrapperDiamond />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenu;
|
||||
@@ -0,0 +1,178 @@
|
||||
import React, { createRef } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import Card from 'shared/components/Card';
|
||||
import CardComposer from 'shared/components/CardComposer';
|
||||
import LabelColors from 'shared/constants/labelColors';
|
||||
import List, { ListCards } from './index';
|
||||
|
||||
export default {
|
||||
component: List,
|
||||
title: 'List',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff', default: true },
|
||||
{ name: 'gray', value: '#f8f8f8' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const labelData = [
|
||||
{
|
||||
labelId: 'development',
|
||||
name: 'Development',
|
||||
color: LabelColors.BLUE,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
labelId: 'general',
|
||||
name: 'General',
|
||||
color: LabelColors.PINK,
|
||||
active: false,
|
||||
},
|
||||
];
|
||||
|
||||
const createCard = () => {
|
||||
const $ref = createRef<HTMLDivElement>();
|
||||
return (
|
||||
<Card
|
||||
cardId="1"
|
||||
listId="1"
|
||||
description="hello!"
|
||||
ref={$ref}
|
||||
title="Hello, world"
|
||||
onClick={action('on click')}
|
||||
onContextMenu={action('on context click')}
|
||||
watched
|
||||
labels={labelData}
|
||||
checklists={{ complete: 1, total: 4 }}
|
||||
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<List
|
||||
id=""
|
||||
name="General"
|
||||
isComposerOpen={false}
|
||||
onSaveName={action('on save name')}
|
||||
onOpenComposer={action('on open composer')}
|
||||
tasks={[]}
|
||||
>
|
||||
<ListCards>
|
||||
<CardComposer
|
||||
onClose={() => {
|
||||
console.log('close!');
|
||||
}}
|
||||
onCreateCard={name => {
|
||||
console.log(name);
|
||||
}}
|
||||
isOpen={false}
|
||||
/>
|
||||
</ListCards>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithCardComposer = () => {
|
||||
return (
|
||||
<List
|
||||
id="1"
|
||||
name="General"
|
||||
isComposerOpen
|
||||
onSaveName={action('on save name')}
|
||||
onOpenComposer={action('on open composer')}
|
||||
tasks={[]}
|
||||
>
|
||||
<ListCards>
|
||||
<CardComposer
|
||||
onClose={() => {
|
||||
console.log('close!');
|
||||
}}
|
||||
onCreateCard={name => {
|
||||
console.log(name);
|
||||
}}
|
||||
isOpen
|
||||
/>
|
||||
</ListCards>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithCard = () => {
|
||||
const $cardRef: any = createRef();
|
||||
return (
|
||||
<List
|
||||
id="1"
|
||||
name="General"
|
||||
isComposerOpen={false}
|
||||
onSaveName={action('on save name')}
|
||||
onOpenComposer={action('on open composer')}
|
||||
tasks={[]}
|
||||
>
|
||||
<ListCards>
|
||||
<Card
|
||||
cardId="1"
|
||||
listId="1"
|
||||
description="hello!"
|
||||
ref={$cardRef}
|
||||
title="Hello, world"
|
||||
onClick={action('on click')}
|
||||
onContextMenu={action('on context click')}
|
||||
watched
|
||||
labels={labelData}
|
||||
checklists={{ complete: 1, total: 4 }}
|
||||
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
|
||||
/>
|
||||
<CardComposer
|
||||
onClose={() => {
|
||||
console.log('close!');
|
||||
}}
|
||||
onCreateCard={name => {
|
||||
console.log(name);
|
||||
}}
|
||||
isOpen={false}
|
||||
/>
|
||||
</ListCards>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
export const WithCardAndComposer = () => {
|
||||
const $cardRef: any = createRef();
|
||||
return (
|
||||
<List
|
||||
id="1"
|
||||
name="General"
|
||||
isComposerOpen
|
||||
onSaveName={action('on save name')}
|
||||
onOpenComposer={action('on open composer')}
|
||||
tasks={[]}
|
||||
>
|
||||
<ListCards>
|
||||
<Card
|
||||
cardId="1"
|
||||
listId="1"
|
||||
description="hello!"
|
||||
ref={$cardRef}
|
||||
title="Hello, world"
|
||||
onClick={action('on click')}
|
||||
onContextMenu={action('on context click')}
|
||||
watched
|
||||
labels={labelData}
|
||||
checklists={{ complete: 1, total: 4 }}
|
||||
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
|
||||
/>
|
||||
<CardComposer
|
||||
onClose={() => {
|
||||
console.log('close!');
|
||||
}}
|
||||
onCreateCard={name => {
|
||||
console.log(name);
|
||||
}}
|
||||
isOpen
|
||||
/>
|
||||
</ListCards>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
|
||||
export const Container = styled.div`
|
||||
width: 272px;
|
||||
margin: 0 4px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const AddCardContainer = styled.div`
|
||||
min-height: 38px;
|
||||
max-height: 38px;
|
||||
display: ${props => (props.hidden ? 'none' : 'flex')};
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const AddCardButton = styled.a`
|
||||
border-radius: 3px;
|
||||
color: #5e6c84;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
flex: 1 0 auto;
|
||||
margin: 2px 8px 8px 8px;
|
||||
padding: 4px 8px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
background-color: rgba(9, 30, 66, 0.08);
|
||||
color: #172b4d;
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
export const Wrapper = styled.div`
|
||||
// background-color: #ebecf0;
|
||||
// background: rgb(244, 245, 247);
|
||||
background: #10163a;
|
||||
color: #c2c6dc;
|
||||
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
position: relative;
|
||||
white-space: normal;
|
||||
`;
|
||||
|
||||
export const HeaderEditTarget = styled.div<{ isHidden: boolean }>`
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: ${props => (props.isHidden ? 'none' : 'block')};
|
||||
`;
|
||||
|
||||
export const HeaderName = styled(TextareaAutosize)`
|
||||
font-family: 'Droid Sans';
|
||||
border: none;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
box-shadow: none;
|
||||
font-weight: 600;
|
||||
margin: -4px 0;
|
||||
padding: 4px 8px;
|
||||
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
text-transform: none;
|
||||
text-indent: 0px;
|
||||
text-shadow: none;
|
||||
flex-direction: column;
|
||||
text-align: start;
|
||||
|
||||
color: #c2c6dc;
|
||||
`;
|
||||
|
||||
export const Header = styled.div<{ isEditing: boolean }>`
|
||||
flex: 0 0 auto;
|
||||
padding: 10px 8px;
|
||||
position: relative;
|
||||
min-height: 20px;
|
||||
padding-right: 36px;
|
||||
|
||||
${props =>
|
||||
props.isEditing &&
|
||||
css`
|
||||
& ${HeaderName} {
|
||||
background: #fff;
|
||||
border: none;
|
||||
box-shadow: inset 0 0 0 2px #0079bf;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const AddCardButtonText = styled.span`
|
||||
padding-left: 5px;
|
||||
font-family: 'Droid Sans';
|
||||
`;
|
||||
|
||||
export const ListCards = styled.div`
|
||||
margin: 0 4px;
|
||||
padding: 0 4px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 30px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
@@ -0,0 +1,101 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import {
|
||||
Container,
|
||||
Wrapper,
|
||||
Header,
|
||||
HeaderName,
|
||||
HeaderEditTarget,
|
||||
AddCardContainer,
|
||||
AddCardButton,
|
||||
AddCardButtonText,
|
||||
ListCards,
|
||||
} from './Styles';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
id: string;
|
||||
name: string;
|
||||
onSaveName: (name: string) => void;
|
||||
isComposerOpen: boolean;
|
||||
onOpenComposer: (id: string) => void;
|
||||
tasks: Task[];
|
||||
wrapperProps?: any;
|
||||
headerProps?: any;
|
||||
index?: number;
|
||||
};
|
||||
|
||||
const List = React.forwardRef(
|
||||
(
|
||||
{ id, name, onSaveName, isComposerOpen, onOpenComposer, children, wrapperProps, headerProps }: Props,
|
||||
$wrapperRef: any,
|
||||
) => {
|
||||
const [listName, setListName] = useState(name);
|
||||
const [isEditingTitle, setEditingTitle] = useState(false);
|
||||
const $listNameRef: any = useRef<HTMLTextAreaElement>();
|
||||
|
||||
const onClick = () => {
|
||||
setEditingTitle(true);
|
||||
if ($listNameRef) {
|
||||
$listNameRef.current.select();
|
||||
}
|
||||
};
|
||||
const onBlur = () => {
|
||||
setEditingTitle(false);
|
||||
onSaveName(listName);
|
||||
};
|
||||
const onEscape = () => {
|
||||
$listNameRef.current.blur();
|
||||
};
|
||||
const onChange = (event: React.FormEvent<HTMLTextAreaElement>): void => {
|
||||
setListName(event.currentTarget.value);
|
||||
};
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
$listNameRef.current.blur();
|
||||
}
|
||||
};
|
||||
useOnEscapeKeyDown(isEditingTitle, onEscape);
|
||||
|
||||
return (
|
||||
<Container ref={$wrapperRef} {...wrapperProps}>
|
||||
<Wrapper>
|
||||
<Header {...headerProps} isEditing={isEditingTitle}>
|
||||
<HeaderEditTarget onClick={onClick} isHidden={isEditingTitle} />
|
||||
<HeaderName
|
||||
ref={$listNameRef}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
spellCheck={false}
|
||||
value={listName}
|
||||
/>
|
||||
</Header>
|
||||
{children && children}
|
||||
<AddCardContainer hidden={isComposerOpen}>
|
||||
<AddCardButton onClick={() => onOpenComposer(id)}>
|
||||
<FontAwesomeIcon icon={faPlus} size="xs" color="#42526e" />
|
||||
<AddCardButtonText>Add another card</AddCardButtonText>
|
||||
</AddCardButton>
|
||||
</AddCardContainer>
|
||||
</Wrapper>
|
||||
</Container>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
List.defaultProps = {
|
||||
children: null,
|
||||
isComposerOpen: false,
|
||||
wrapperProps: {},
|
||||
headerProps: {},
|
||||
};
|
||||
|
||||
List.displayName = 'List';
|
||||
export default List;
|
||||
|
||||
export { ListCards };
|
||||
@@ -0,0 +1,184 @@
|
||||
import React, { useState } from 'react';
|
||||
import Lists from './index';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
component: Lists,
|
||||
title: 'Lists',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff', default: true },
|
||||
{ name: 'gray', value: '#f8f8f8' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const initialListsData = {
|
||||
columns: {
|
||||
'column-1': {
|
||||
taskGroupID: 'column-1',
|
||||
name: 'General',
|
||||
taskIds: ['task-3', 'task-4', 'task-1', 'task-2'],
|
||||
position: 1,
|
||||
tasks: [],
|
||||
},
|
||||
'column-2': {
|
||||
taskGroupID: 'column-2',
|
||||
name: 'Development',
|
||||
taskIds: [],
|
||||
position: 2,
|
||||
tasks: [],
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
'task-1': {
|
||||
taskID: 'task-1',
|
||||
taskGroupID: 'column-1',
|
||||
name: 'Create roadmap',
|
||||
position: 2,
|
||||
labels: [],
|
||||
},
|
||||
'task-2': {
|
||||
taskID: 'task-2',
|
||||
taskGroupID: 'column-1',
|
||||
position: 1,
|
||||
name: 'Create authentication',
|
||||
labels: [],
|
||||
},
|
||||
'task-3': {
|
||||
taskID: 'task-3',
|
||||
taskGroupID: 'column-1',
|
||||
position: 3,
|
||||
name: 'Create login',
|
||||
labels: [],
|
||||
},
|
||||
'task-4': {
|
||||
taskID: 'task-4',
|
||||
taskGroupID: 'column-1',
|
||||
position: 4,
|
||||
name: 'Create plugins',
|
||||
labels: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const [listsData, setListsData] = useState(initialListsData);
|
||||
const onCardDrop = (droppedTask: any) => {
|
||||
console.log(droppedTask);
|
||||
const newState = {
|
||||
...listsData,
|
||||
tasks: {
|
||||
...listsData.tasks,
|
||||
[droppedTask.id]: droppedTask,
|
||||
},
|
||||
};
|
||||
console.log(newState);
|
||||
setListsData(newState);
|
||||
};
|
||||
const onListDrop = (droppedColumn: any) => {
|
||||
const newState = {
|
||||
...listsData,
|
||||
columns: {
|
||||
...listsData.columns,
|
||||
[droppedColumn.id]: droppedColumn,
|
||||
},
|
||||
};
|
||||
setListsData(newState);
|
||||
};
|
||||
return (
|
||||
<Lists
|
||||
{...listsData}
|
||||
onQuickEditorOpen={action('card composer open')}
|
||||
onCardDrop={onCardDrop}
|
||||
onListDrop={onListDrop}
|
||||
onCardCreate={action('card create')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const createColumn = (id: any, name: any, position: any) => {
|
||||
return {
|
||||
taskGroupID: id,
|
||||
name,
|
||||
position,
|
||||
tasks: [],
|
||||
};
|
||||
};
|
||||
|
||||
const initialListsDataLarge = {
|
||||
columns: {
|
||||
'column-1': createColumn('column-1', 'General', 1),
|
||||
'column-2': createColumn('column-2', 'General', 2),
|
||||
'column-3': createColumn('column-3', 'General', 3),
|
||||
'column-4': createColumn('column-4', 'General', 4),
|
||||
'column-5': createColumn('column-5', 'General', 5),
|
||||
'column-6': createColumn('column-6', 'General', 6),
|
||||
'column-7': createColumn('column-7', 'General', 7),
|
||||
'column-8': createColumn('column-8', 'General', 8),
|
||||
'column-9': createColumn('column-9', 'General', 9),
|
||||
},
|
||||
tasks: {
|
||||
'task-1': {
|
||||
taskID: 'task-1',
|
||||
taskGroupID: 'column-1',
|
||||
name: 'Create roadmap',
|
||||
position: 2,
|
||||
labels: [],
|
||||
},
|
||||
'task-2': {
|
||||
taskID: 'task-2',
|
||||
taskGroupID: 'column-1',
|
||||
position: 1,
|
||||
name: 'Create authentication',
|
||||
labels: [],
|
||||
},
|
||||
'task-3': {
|
||||
taskID: 'task-3',
|
||||
taskGroupID: 'column-1',
|
||||
position: 3,
|
||||
name: 'Create login',
|
||||
labels: [],
|
||||
},
|
||||
'task-4': {
|
||||
taskID: 'task-4',
|
||||
taskGroupID: 'column-1',
|
||||
position: 4,
|
||||
name: 'Create plugins',
|
||||
labels: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ListsWithManyList = () => {
|
||||
const [listsData, setListsData] = useState(initialListsDataLarge);
|
||||
const onCardDrop = (droppedTask: any) => {
|
||||
const newState = {
|
||||
...listsData,
|
||||
tasks: {
|
||||
...listsData.tasks,
|
||||
[droppedTask.id]: droppedTask,
|
||||
},
|
||||
};
|
||||
setListsData(newState);
|
||||
};
|
||||
const onListDrop = (droppedColumn: any) => {
|
||||
const newState = {
|
||||
...listsData,
|
||||
columns: {
|
||||
...listsData.columns,
|
||||
[droppedColumn.id]: droppedColumn,
|
||||
},
|
||||
};
|
||||
setListsData(newState);
|
||||
};
|
||||
return (
|
||||
<Lists
|
||||
{...listsData}
|
||||
onQuickEditorOpen={action('card composer open')}
|
||||
onCardCreate={action('card create')}
|
||||
onCardDrop={onCardDrop}
|
||||
onListDrop={onListDrop}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
flex-grow: 1;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 8px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 8px;
|
||||
`;
|
||||
@@ -0,0 +1,196 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
import { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/arrays';
|
||||
import List, { ListCards } from 'shared/components/List';
|
||||
import Card from 'shared/components/Card';
|
||||
import { Container } from './Styles';
|
||||
import CardComposer from 'shared/components/CardComposer';
|
||||
|
||||
const getNewDraggablePosition = (afterDropDraggables: any, draggableIndex: any) => {
|
||||
const prevDraggable = afterDropDraggables[draggableIndex - 1];
|
||||
const nextDraggable = afterDropDraggables[draggableIndex + 1];
|
||||
if (!prevDraggable && !nextDraggable) {
|
||||
return 1;
|
||||
}
|
||||
if (!prevDraggable) {
|
||||
return nextDraggable.position - 1;
|
||||
}
|
||||
if (!nextDraggable) {
|
||||
return prevDraggable.position + 1;
|
||||
}
|
||||
const newPos = (prevDraggable.position + nextDraggable.position) / 2.0;
|
||||
return newPos;
|
||||
};
|
||||
|
||||
const getSortedDraggables = (draggables: any) => {
|
||||
return draggables.sort((a: any, b: any) => a.position - b.position);
|
||||
};
|
||||
|
||||
const isPositionChanged = (source: any, destination: any) => {
|
||||
if (!destination) return false;
|
||||
const isSameList = destination.droppableId === source.droppableId;
|
||||
const isSamePosition = destination.index === source.index;
|
||||
return !isSameList || !isSamePosition;
|
||||
};
|
||||
|
||||
const getAfterDropDraggableList = (
|
||||
beforeDropDraggables: any,
|
||||
droppedDraggable: any,
|
||||
isList: any,
|
||||
isSameList: any,
|
||||
destination: any,
|
||||
) => {
|
||||
if (isList) {
|
||||
return moveItemWithinArray(beforeDropDraggables, droppedDraggable, destination.index);
|
||||
}
|
||||
return isSameList
|
||||
? moveItemWithinArray(beforeDropDraggables, droppedDraggable, destination.index)
|
||||
: insertItemIntoArray(beforeDropDraggables, droppedDraggable, destination.index);
|
||||
};
|
||||
|
||||
interface Columns {
|
||||
[key: string]: TaskGroup;
|
||||
}
|
||||
interface Tasks {
|
||||
[key: string]: RemoteTask;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
columns: Columns;
|
||||
tasks: Tasks;
|
||||
onCardDrop: any;
|
||||
onListDrop: any;
|
||||
onCardCreate: (taskGroupID: string, name: string) => void;
|
||||
onQuickEditorOpen: (e: ContextMenuEvent) => void;
|
||||
};
|
||||
|
||||
type OnDragEndProps = {
|
||||
draggableId: any;
|
||||
source: any;
|
||||
destination: any;
|
||||
type: any;
|
||||
};
|
||||
|
||||
const Lists = ({ columns, tasks, onCardDrop, onListDrop, onCardCreate, onQuickEditorOpen }: Props) => {
|
||||
const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
|
||||
if (typeof destination === 'undefined') return;
|
||||
if (!isPositionChanged(source, destination)) return;
|
||||
|
||||
const isList = type === 'column';
|
||||
const isSameList = destination.droppableId === source.droppableId;
|
||||
const droppedDraggable = isList ? columns[draggableId] : tasks[draggableId];
|
||||
const beforeDropDraggables = isList
|
||||
? getSortedDraggables(Object.values(columns))
|
||||
: getSortedDraggables(Object.values(tasks).filter((t: any) => t.taskGroupID === destination.droppableId));
|
||||
|
||||
const afterDropDraggables = getAfterDropDraggableList(
|
||||
beforeDropDraggables,
|
||||
droppedDraggable,
|
||||
isList,
|
||||
isSameList,
|
||||
destination,
|
||||
);
|
||||
const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
|
||||
|
||||
if (isList) {
|
||||
onListDrop({
|
||||
...droppedDraggable,
|
||||
position: newPosition,
|
||||
});
|
||||
} else {
|
||||
const newCard = {
|
||||
...droppedDraggable,
|
||||
position: newPosition,
|
||||
taskGroupID: destination.droppableId,
|
||||
};
|
||||
onCardDrop(newCard);
|
||||
}
|
||||
};
|
||||
|
||||
const orderedColumns = getSortedDraggables(Object.values(columns));
|
||||
|
||||
const [currentComposer, setCurrentComposer] = useState('');
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable direction="horizontal" type="column" droppableId="root">
|
||||
{provided => (
|
||||
<Container {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{orderedColumns.map((column: TaskGroup, index: number) => {
|
||||
const columnCards = getSortedDraggables(
|
||||
Object.values(tasks).filter((t: any) => t.taskGroupID === column.taskGroupID),
|
||||
);
|
||||
return (
|
||||
<Draggable draggableId={column.taskGroupID} key={column.taskGroupID} index={index}>
|
||||
{columnDragProvided => (
|
||||
<List
|
||||
id={column.taskGroupID}
|
||||
name={column.name}
|
||||
key={column.taskGroupID}
|
||||
onOpenComposer={id => setCurrentComposer(id)}
|
||||
isComposerOpen={currentComposer === column.taskGroupID}
|
||||
onSaveName={name => console.log(name)}
|
||||
index={index}
|
||||
tasks={columnCards}
|
||||
ref={columnDragProvided.innerRef}
|
||||
wrapperProps={columnDragProvided.draggableProps}
|
||||
headerProps={columnDragProvided.dragHandleProps}
|
||||
>
|
||||
<Droppable type="tasks" droppableId={column.taskGroupID}>
|
||||
{columnDropProvided => (
|
||||
<ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
|
||||
{columnCards.map((task: RemoteTask, taskIndex: any) => {
|
||||
return (
|
||||
<Draggable key={task.taskID} draggableId={task.taskID} index={taskIndex}>
|
||||
{taskProvided => {
|
||||
return (
|
||||
<Card
|
||||
wrapperProps={{
|
||||
...taskProvided.draggableProps,
|
||||
...taskProvided.dragHandleProps,
|
||||
}}
|
||||
ref={taskProvided.innerRef}
|
||||
cardId={task.taskID}
|
||||
listId={column.taskGroupID}
|
||||
description=""
|
||||
title={task.name}
|
||||
labels={task.labels}
|
||||
onClick={e => console.log(e)}
|
||||
onContextMenu={onQuickEditorOpen}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{columnDropProvided.placeholder}
|
||||
|
||||
{currentComposer === column.taskGroupID && (
|
||||
<CardComposer
|
||||
onClose={() => {
|
||||
setCurrentComposer('');
|
||||
}}
|
||||
onCreateCard={name => {
|
||||
setCurrentComposer('');
|
||||
onCardCreate(column.taskGroupID, name);
|
||||
}}
|
||||
isOpen={true}
|
||||
/>
|
||||
)}
|
||||
</ListCards>
|
||||
)}
|
||||
</Droppable>
|
||||
</List>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</Container>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default Lists;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useState } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import NormalizeStyles from 'App/NormalizeStyles';
|
||||
import BaseStyles from 'App/BaseStyles';
|
||||
import styled from 'styled-components';
|
||||
import Login from './index';
|
||||
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
export default {
|
||||
component: Login,
|
||||
title: 'Login',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff' },
|
||||
{ name: 'gray', value: '#cdd3e1', default: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
`;
|
||||
|
||||
const LoginWrapper = styled.div`
|
||||
width: 60%;
|
||||
`;
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<Container>
|
||||
<LoginWrapper>
|
||||
<Login onSubmit={action('on submit')} />
|
||||
</LoginWrapper>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithSubmission = () => {
|
||||
const onSubmit = async (data: LoginFormData, setComplete: (val: boolean) => void, setError: any) => {
|
||||
await sleep(2000);
|
||||
if (data.username !== 'test' || data.password !== 'test') {
|
||||
setError('username', 'invalid', 'Invalid username');
|
||||
setError('password', 'invalid', 'Invalid password');
|
||||
}
|
||||
setComplete(true);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<Container>
|
||||
<LoginWrapper>
|
||||
<Login onSubmit={onSubmit} />
|
||||
</LoginWrapper>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
background: #eff2f7;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
export const Column = styled.div`
|
||||
width: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const LoginFormWrapper = styled.div`
|
||||
background: #10163a;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LoginFormContainer = styled.div`
|
||||
min-height: 505px;
|
||||
padding: 2rem;
|
||||
`;
|
||||
|
||||
export const Title = styled.h1`
|
||||
color: #ebeefd;
|
||||
font-size: 18px;
|
||||
margin-bottom: 14px;
|
||||
`;
|
||||
|
||||
export const SubTitle = styled.h2`
|
||||
color: #c2c6dc;
|
||||
font-size: 14px;
|
||||
margin-bottom: 14px;
|
||||
`;
|
||||
export const Form = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const FormLabel = styled.label`
|
||||
color: #c2c6dc;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
margin-top: 14px;
|
||||
`;
|
||||
|
||||
export const FormTextInput = styled.input`
|
||||
width: 100%;
|
||||
background: #262c49;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
margin-top: 4px;
|
||||
padding: 0.7rem 1rem 0.7rem 3rem;
|
||||
font-size: 1rem;
|
||||
color: #c2c6dc;
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
export const FormIcon = styled.div`
|
||||
top: 30px;
|
||||
left: 16px;
|
||||
position: absolute;
|
||||
`;
|
||||
|
||||
export const FormError = styled.span`
|
||||
font-size: 0.875rem;
|
||||
color: rgb(234, 84, 85);
|
||||
`;
|
||||
|
||||
export const LoginButton = styled.input`
|
||||
padding: 0.75rem 2rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 6px;
|
||||
background: rgb(115, 103, 240);
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ActionButtons = styled.div`
|
||||
margin-top: 17.5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const RegisterButton = styled.button`
|
||||
padding: 0.679rem 2rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgb(115, 103, 240);
|
||||
background: transparent;
|
||||
font-size: 1rem;
|
||||
color: rgba(115, 103, 240);
|
||||
cursor: pointer;
|
||||
`;
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { useState } from 'react';
|
||||
import AccessAccount from 'shared/undraw/AccessAccount';
|
||||
import { User, Lock } from 'shared/icons';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import {
|
||||
Form,
|
||||
ActionButtons,
|
||||
RegisterButton,
|
||||
LoginButton,
|
||||
FormError,
|
||||
FormIcon,
|
||||
FormLabel,
|
||||
FormTextInput,
|
||||
Wrapper,
|
||||
Column,
|
||||
LoginFormWrapper,
|
||||
LoginFormContainer,
|
||||
Title,
|
||||
SubTitle,
|
||||
} from './Styles';
|
||||
|
||||
const Login = ({ onSubmit }: LoginProps) => {
|
||||
const [isComplete, setComplete] = useState(true);
|
||||
const { register, handleSubmit, errors, setError, formState } = useForm<LoginFormData>();
|
||||
console.log(formState);
|
||||
const loginSubmit = (data: LoginFormData) => {
|
||||
setComplete(false);
|
||||
onSubmit(data, setComplete, setError);
|
||||
};
|
||||
return (
|
||||
<Wrapper>
|
||||
<Column>
|
||||
<AccessAccount width={275} height={250} />
|
||||
</Column>
|
||||
<Column>
|
||||
<LoginFormWrapper>
|
||||
<LoginFormContainer>
|
||||
<Title>Login</Title>
|
||||
<SubTitle>Welcome back, please login into your account.</SubTitle>
|
||||
<Form onSubmit={handleSubmit(loginSubmit)}>
|
||||
<FormLabel htmlFor="username">
|
||||
Username
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
ref={register({ required: 'Username is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<User color="#c2c6dc" size={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.username && <FormError>{errors.username.message}</FormError>}
|
||||
<FormLabel htmlFor="password">
|
||||
Password
|
||||
<FormTextInput
|
||||
type="text"
|
||||
id="password"
|
||||
name="password"
|
||||
ref={register({ required: 'Password is required' })}
|
||||
/>
|
||||
<FormIcon>
|
||||
<Lock color="#c2c6dc" size={20} />
|
||||
</FormIcon>
|
||||
</FormLabel>
|
||||
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||
|
||||
<ActionButtons>
|
||||
<RegisterButton>Register</RegisterButton>
|
||||
<LoginButton type="submit" value="Login" disabled={!isComplete} />
|
||||
</ActionButtons>
|
||||
</Form>
|
||||
</LoginFormContainer>
|
||||
</LoginFormWrapper>
|
||||
</Column>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import NormalizeStyles from 'App/NormalizeStyles';
|
||||
import BaseStyles from 'App/BaseStyles';
|
||||
import { Home, Stack, Users, Question } from 'shared/icons';
|
||||
import Navbar, { ActionButton, ButtonContainer, PrimaryLogo } from './index';
|
||||
|
||||
export default {
|
||||
component: Navbar,
|
||||
title: 'Navbar',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff', default: true },
|
||||
{ name: 'gray', value: '#cdd3e1' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const MainContent = styled.div`
|
||||
padding: 0 0 50px 80px;
|
||||
`;
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<Navbar>
|
||||
<PrimaryLogo />
|
||||
<ButtonContainer>
|
||||
<ActionButton name="Home">
|
||||
<Home size={28} color="#c2c6dc" />
|
||||
</ActionButton>
|
||||
<ActionButton name="Home">
|
||||
<Home size={28} color="#c2c6dc" />
|
||||
</ActionButton>
|
||||
</ButtonContainer>
|
||||
</Navbar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
export const LogoWrapper = styled.div`
|
||||
margin: 20px 0px 20px;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
padding-left: 64px;
|
||||
color: rgb(222, 235, 255);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: color 0.1s ease 0s;
|
||||
`;
|
||||
|
||||
export const Logo = styled.div`
|
||||
position: absolute;
|
||||
left: 19px;
|
||||
`;
|
||||
|
||||
export const LogoTitle = styled.div`
|
||||
position: relative;
|
||||
right: 12px;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
transition: right 0.1s ease 0s, visibility, opacity, transform 0.25s ease;
|
||||
color: #7367f0;
|
||||
`;
|
||||
export const ActionContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ActionButtonTitle = styled.span`
|
||||
position: relative;
|
||||
visibility: hidden;
|
||||
left: -5px;
|
||||
opacity: 0;
|
||||
font-weight: 600;
|
||||
transition: left 0.1s ease 0s, visibility, opacity, transform 0.25s ease;
|
||||
|
||||
font-size: 18px;
|
||||
color: #c2c6dc;
|
||||
`;
|
||||
export const IconWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.25s ease;
|
||||
`;
|
||||
|
||||
export const ActionButtonContainer = styled.div`
|
||||
padding: 0 12px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ActionButtonWrapper = styled.div<{ active?: boolean }>`
|
||||
${props =>
|
||||
props.active &&
|
||||
css`
|
||||
background: rgb(115, 103, 240);
|
||||
box-shadow: 0 0 10px 1px rgba(115, 103, 240, 0.7);
|
||||
`}
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:hover ${ActionButtonTitle} {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
&:hover ${IconWrapper} {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled.aside`
|
||||
z-index: 100;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
overflow-x: hidden;
|
||||
height: 100vh;
|
||||
width: 80px;
|
||||
transform: translateZ(0px);
|
||||
background: #10163a;
|
||||
transition: all 0.1s ease 0s;
|
||||
|
||||
&:hover {
|
||||
width: 260px;
|
||||
box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 50px 0px;
|
||||
}
|
||||
&:hover ${LogoTitle} {
|
||||
right: 0px;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
&:hover ${ActionButtonTitle} {
|
||||
left: 15px;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { Citadel } from 'shared/icons';
|
||||
import {
|
||||
Container,
|
||||
LogoWrapper,
|
||||
IconWrapper,
|
||||
Logo,
|
||||
LogoTitle,
|
||||
ActionContainer,
|
||||
ActionButtonContainer,
|
||||
ActionButtonWrapper,
|
||||
ActionButtonTitle,
|
||||
} from './Styles';
|
||||
|
||||
type ActionButtonProps = {
|
||||
name: string;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
export const ActionButton: React.FC<ActionButtonProps> = ({ name, active, children }) => {
|
||||
return (
|
||||
<ActionButtonWrapper active={active ?? false}>
|
||||
<IconWrapper>{children}</IconWrapper>
|
||||
<ActionButtonTitle>{name}</ActionButtonTitle>
|
||||
</ActionButtonWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonContainer: React.FC = ({ children }) => (
|
||||
<ActionContainer>
|
||||
<ActionButtonContainer>{children}</ActionButtonContainer>
|
||||
</ActionContainer>
|
||||
);
|
||||
|
||||
export const PrimaryLogo = () => {
|
||||
return (
|
||||
<LogoWrapper>
|
||||
<Logo>
|
||||
<Citadel size={42} />
|
||||
</Logo>
|
||||
<LogoTitle>Citadel</LogoTitle>
|
||||
</LogoWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Navbar: React.FC = ({ children }) => {
|
||||
return <Container>{children}</Container>;
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { useState } from 'react';
|
||||
import LabelColors from 'shared/constants/labelColors';
|
||||
import { Checkmark } from 'shared/icons';
|
||||
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
|
||||
|
||||
type Props = {
|
||||
label: Label;
|
||||
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
|
||||
};
|
||||
const LabelManager = ({ label, onLabelEdit }: Props) => {
|
||||
const [currentLabel, setCurrentLabel] = useState('');
|
||||
return (
|
||||
<EditLabelForm>
|
||||
<FieldLabel>Name</FieldLabel>
|
||||
<FieldName id="labelName" type="text" name="name" value={currentLabel} />
|
||||
<FieldLabel>Select a color</FieldLabel>
|
||||
<div>
|
||||
{Object.values(LabelColors).map(labelColor => (
|
||||
<LabelBox color={labelColor}>
|
||||
<Checkmark color="#fff" size={12} />
|
||||
</LabelBox>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<SaveButton type="submit" value="Save" />
|
||||
<DeleteButton type="submit" value="Delete" />
|
||||
</div>
|
||||
</EditLabelForm>
|
||||
);
|
||||
};
|
||||
export default LabelManager;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Pencil, Checkmark } from 'shared/icons';
|
||||
|
||||
import { LabelSearch, ActiveIcon, Labels, Label, CardLabel, Section, SectionTitle, LabelIcon } from './Styles';
|
||||
|
||||
type Props = {
|
||||
labels?: Label[];
|
||||
onLabelToggle: (labelId: string) => void;
|
||||
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
|
||||
};
|
||||
const LabelManager = ({ labels, onLabelToggle, onLabelEdit }: Props) => {
|
||||
const [currentLabel, setCurrentLabel] = useState('');
|
||||
return (
|
||||
<>
|
||||
<LabelSearch type="text" />
|
||||
<Section>
|
||||
<SectionTitle>Labels</SectionTitle>
|
||||
<Labels>
|
||||
{labels &&
|
||||
labels.map(label => (
|
||||
<Label>
|
||||
<LabelIcon>
|
||||
<Pencil />
|
||||
</LabelIcon>
|
||||
<CardLabel
|
||||
key={label.labelId}
|
||||
color={label.color}
|
||||
active={currentLabel === label.labelId}
|
||||
onMouseEnter={() => {
|
||||
setCurrentLabel(label.labelId);
|
||||
}}
|
||||
onClick={() => onLabelToggle(label.labelId)}
|
||||
>
|
||||
{label.name}
|
||||
{label.active && (
|
||||
<ActiveIcon>
|
||||
<Checkmark color="#fff" />
|
||||
</ActiveIcon>
|
||||
)}
|
||||
</CardLabel>
|
||||
</Label>
|
||||
))}
|
||||
</Labels>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default LabelManager;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { createRef, useState } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import LabelColors from 'shared/constants/labelColors';
|
||||
import MenuTypes from 'shared/constants/menuTypes';
|
||||
import PopupMenu from './index';
|
||||
|
||||
export default {
|
||||
component: PopupMenu,
|
||||
title: 'PopupMenu',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff', default: true },
|
||||
{ name: 'gray', value: '#f8f8f8' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const labelData = [
|
||||
{
|
||||
labelId: 'development',
|
||||
name: 'Development',
|
||||
color: LabelColors.BLUE,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
labelId: 'general',
|
||||
name: 'General',
|
||||
color: LabelColors.PINK,
|
||||
active: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const LabelsPopup = () => {
|
||||
const [isPopupOpen, setPopupOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
{isPopupOpen && (
|
||||
<PopupMenu
|
||||
title="Label"
|
||||
menuType={MenuTypes.LABEL_MANAGER}
|
||||
top={10}
|
||||
onClose={() => setPopupOpen(false)}
|
||||
left={10}
|
||||
onLabelEdit={action('label edit')}
|
||||
onLabelToggle={action('label toggle')}
|
||||
labels={labelData}
|
||||
/>
|
||||
)}
|
||||
<button type="submit" onClick={() => setPopupOpen(true)}>
|
||||
Open
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const LabelsLabelEditor = () => {
|
||||
const [isPopupOpen, setPopupOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
{isPopupOpen && (
|
||||
<PopupMenu
|
||||
title="Change Label"
|
||||
menuType={MenuTypes.LABEL_EDITOR}
|
||||
top={10}
|
||||
onClose={() => setPopupOpen(false)}
|
||||
left={10}
|
||||
onLabelEdit={action('label edit')}
|
||||
onLabelToggle={action('label toggle')}
|
||||
labels={labelData}
|
||||
/>
|
||||
)}
|
||||
<button type="submit" onClick={() => setPopupOpen(true)}>
|
||||
Open
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
|
||||
export const Container = styled.div<{ top: number; left: number; ref: any }>`
|
||||
left: ${props => props.left}px;
|
||||
top: ${props => props.top}px;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 8px 16px -4px rgba(9, 30, 66, 0.25), 0 0 0 1px rgba(9, 30, 66, 0.08);
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
width: 304px;
|
||||
z-index: 70;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Header = styled.div`
|
||||
height: 40px;
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const HeaderTitle = styled.span`
|
||||
box-sizing: border-box;
|
||||
color: #5e6c84;
|
||||
display: block;
|
||||
line-height: 40px;
|
||||
border-bottom: 1px solid rgba(9, 30, 66, 0.13);
|
||||
margin: 0 12px;
|
||||
overflow: hidden;
|
||||
padding: 0 32px;
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export const Content = styled.div`
|
||||
max-height: 632px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0 12px 12px;
|
||||
`;
|
||||
export const LabelSearch = styled.input`
|
||||
margin: 4px 0 12px;
|
||||
width: 100%;
|
||||
background-color: #fafbfc;
|
||||
border: none;
|
||||
box-shadow: inset 0 0 0 2px #dfe1e6;
|
||||
color: #172b4d;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
font-family: 'Droid Sans';
|
||||
font-weight: 400;
|
||||
transition-property: background-color, border-color, box-shadow;
|
||||
transition-duration: 85ms;
|
||||
transition-timing-function: ease;
|
||||
`;
|
||||
|
||||
export const Section = styled.div`
|
||||
margin-top: 12px;
|
||||
`;
|
||||
|
||||
export const SectionTitle = styled.h4`
|
||||
color: #5e6c84;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 16px;
|
||||
margin-top: 16px;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
export const Labels = styled.ul`
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
export const Label = styled.li`
|
||||
padding-right: 36px;
|
||||
position: relative;
|
||||
`;
|
||||
export const CardLabel = styled.span<{ active: boolean; color: string }>`
|
||||
${props =>
|
||||
props.active &&
|
||||
css`
|
||||
margin-left: 4px;
|
||||
box-shadow: -8px 0 ${mixin.darken(props.color, 0.15)};
|
||||
border-radius: 3px;
|
||||
`}
|
||||
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
margin: 0 0 4px;
|
||||
min-height: 20px;
|
||||
padding: 6px 12px;
|
||||
position: relative;
|
||||
transition: padding 85ms, margin 85ms, box-shadow 85ms;
|
||||
background-color: ${props => props.color};
|
||||
color: #fff;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const CloseButton = styled.div`
|
||||
padding: 10px 12px 10px 8px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 40;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const LabelIcon = styled.div`
|
||||
border-radius: 3px;
|
||||
padding: 6px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 20px;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(9, 30, 66, 0.08);
|
||||
}
|
||||
`;
|
||||
|
||||
export const ActiveIcon = styled.div`
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
opacity: 0.85;
|
||||
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
width: 20px;
|
||||
`;
|
||||
|
||||
export const EditLabelForm = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const FieldLabel = styled.label`
|
||||
font-weight: 700;
|
||||
color: #5e6c84;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
`;
|
||||
|
||||
export const FieldName = styled.input`
|
||||
margin: 4px 0 12px;
|
||||
width: 100%;
|
||||
background-color: #fafbfc;
|
||||
border: none;
|
||||
box-shadow: inset 0 0 0 2px #dfe1e6;
|
||||
color: #172b4d;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
export const LabelBox = styled.span<{ color: string }>`
|
||||
float: left;
|
||||
height: 32px;
|
||||
margin: 0 8px 8px 0;
|
||||
padding: 0;
|
||||
width: 48px;
|
||||
|
||||
background-color: ${props => props.color};
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const SaveButton = styled.input`
|
||||
background-color: #5aac44;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
ursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
margin: 8px 4px 0 0;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
export const DeleteButton = styled.input`
|
||||
background-color: #cf513d;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
margin: 8px 4px 0 0;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
float: right;
|
||||
`;
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Cross } from 'shared/icons';
|
||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||
import MenuTypes from 'shared/constants/menuTypes';
|
||||
import LabelColors from 'shared/constants/labelColors';
|
||||
import LabelManager from './LabelManager';
|
||||
import LabelEditor from './LabelEditor';
|
||||
import { Container, Header, HeaderTitle, Content, Label, CloseButton } from './Styles';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
top: number;
|
||||
left: number;
|
||||
menuType: number;
|
||||
labels?: Label[];
|
||||
onClose: () => void;
|
||||
|
||||
onLabelToggle: (labelId: string) => void;
|
||||
onLabelEdit: (labelId: string, labelName: string, color: string) => void;
|
||||
};
|
||||
|
||||
const PopupMenu = ({ title, menuType, labels, top, left, onClose, onLabelToggle, onLabelEdit }: Props) => {
|
||||
const $containerRef = useRef();
|
||||
useOnOutsideClick($containerRef, true, onClose, null);
|
||||
|
||||
return (
|
||||
<Container left={left} top={top} ref={$containerRef}>
|
||||
<Header>
|
||||
<HeaderTitle>{title}</HeaderTitle>
|
||||
<CloseButton onClick={() => onClose()}>
|
||||
<Cross />
|
||||
</CloseButton>
|
||||
</Header>
|
||||
<Content>
|
||||
{menuType === MenuTypes.LABEL_MANAGER && (
|
||||
<LabelManager onLabelEdit={onLabelEdit} onLabelToggle={onLabelToggle} labels={labels} />
|
||||
)}
|
||||
{menuType === MenuTypes.LABEL_EDITOR && (
|
||||
<LabelEditor
|
||||
onLabelEdit={onLabelEdit}
|
||||
label={{ active: false, color: LabelColors.GREEN, name: 'General', labelId: 'general' }}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupMenu;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import ProjectGridItem from './';
|
||||
|
||||
export default {
|
||||
component: ProjectGridItem,
|
||||
title: 'ProjectGridItem',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff', default: true },
|
||||
{ name: 'gray', value: '#f8f8f8' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const projectsData = [
|
||||
{ taskGroups: [], teamTitle: 'Personal', projectID: 'aaaa', name: 'Citadel', color: '#aa62e3' },
|
||||
{ taskGroups: [], teamTitle: 'Personal', projectID: 'bbbb', name: 'Editorial Calender', color: '#aa62e3' },
|
||||
{ taskGroups: [], teamTitle: 'Personal', projectID: 'cccc', name: 'New Blog', color: '#aa62e3' },
|
||||
];
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
`;
|
||||
|
||||
const ProjectsWrapper = styled.div`
|
||||
width: 60%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<Container>
|
||||
<ProjectsWrapper>
|
||||
{projectsData.map(project => (
|
||||
<ProjectGridItem project={project} />
|
||||
))}
|
||||
</ProjectsWrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import styled from 'styled-components';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
|
||||
export const ProjectContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const ProjectTitle = styled.span`
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
transition: transform 0.25s ease;
|
||||
text-align: center;
|
||||
`;
|
||||
export const TeamTitle = styled.span`
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
color: #c2c6dc;
|
||||
`;
|
||||
|
||||
export const ProjectWrapper = styled.div<{ color: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px 25px;
|
||||
border-radius: 20px;
|
||||
${mixin.boxShadowCard}
|
||||
background: ${props => mixin.darken(props.color, 0.35)};
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
margin: 0 10px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ProjectWrapper, ProjectContent, ProjectTitle, TeamTitle } from './Styles';
|
||||
|
||||
type Props = {
|
||||
project: Project;
|
||||
};
|
||||
|
||||
const ProjectsList = ({ project }: Props) => {
|
||||
const color = project.color ?? '#c2c6dc';
|
||||
return (
|
||||
<ProjectWrapper color={color}>
|
||||
<ProjectContent>
|
||||
<ProjectTitle>{project.name}</ProjectTitle>
|
||||
<TeamTitle>{project.teamTitle}</TeamTitle>
|
||||
</ProjectContent>
|
||||
</ProjectWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectsList;
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { createRef, useState } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import Card from 'shared/components/Card';
|
||||
import CardComposer from 'shared/components/CardComposer';
|
||||
import LabelColors from 'shared/constants/labelColors';
|
||||
import List, { ListCards } from 'shared/components/List';
|
||||
import QuickCardEditor from 'shared/components/QuickCardEditor';
|
||||
|
||||
export default {
|
||||
component: QuickCardEditor,
|
||||
title: 'QuickCardEditor',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff', default: true },
|
||||
{ name: 'gray', value: '#f8f8f8' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const labelData = [
|
||||
{
|
||||
labelId: 'development',
|
||||
name: 'Development',
|
||||
color: LabelColors.BLUE,
|
||||
active: false,
|
||||
},
|
||||
{
|
||||
labelId: 'general',
|
||||
name: 'General',
|
||||
color: LabelColors.PINK,
|
||||
active: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const Default = () => {
|
||||
const $cardRef: any = createRef();
|
||||
const [isEditorOpen, setEditorOpen] = useState(false);
|
||||
const [top, setTop] = useState(0);
|
||||
const [left, setLeft] = useState(0);
|
||||
return (
|
||||
<>
|
||||
{isEditorOpen && (
|
||||
<QuickCardEditor
|
||||
isOpen={isEditorOpen}
|
||||
listId="1"
|
||||
cardId="1"
|
||||
cardTitle="Hello, world"
|
||||
onCloseEditor={() => setEditorOpen(false)}
|
||||
onEditCard={action('edit card')}
|
||||
onOpenPopup={action('open popup')}
|
||||
onArchiveCard={action('archive card')}
|
||||
labels={labelData}
|
||||
top={top}
|
||||
left={left}
|
||||
/>
|
||||
)}
|
||||
<List
|
||||
id="1"
|
||||
name="General"
|
||||
isComposerOpen={false}
|
||||
onSaveName={action('on save name')}
|
||||
onOpenComposer={action('on open composer')}
|
||||
tasks={[]}
|
||||
>
|
||||
<ListCards>
|
||||
<Card
|
||||
cardId="1"
|
||||
listId="1"
|
||||
description="hello!"
|
||||
ref={$cardRef}
|
||||
title="Hello, world"
|
||||
onClick={action('on click')}
|
||||
onContextMenu={e => {
|
||||
setTop(e.top);
|
||||
setLeft(e.left);
|
||||
setEditorOpen(true);
|
||||
}}
|
||||
watched
|
||||
labels={labelData}
|
||||
checklists={{ complete: 1, total: 4 }}
|
||||
dueDate={{ isPastDue: false, formattedDate: 'Oct 26, 2020' }}
|
||||
/>
|
||||
<CardComposer
|
||||
onClose={() => {
|
||||
console.log('close!');
|
||||
}}
|
||||
onCreateCard={name => {
|
||||
console.log(name);
|
||||
}}
|
||||
isOpen={false}
|
||||
/>
|
||||
</ListCards>
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
|
||||
export const Wrapper = styled.div<{ open: boolean }>`
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
bottom: 0;
|
||||
color: #fff;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
visibility: ${props => (props.open ? 'show' : 'hidden')};
|
||||
`;
|
||||
|
||||
export const Container = styled.div<{ top: number; left: number }>`
|
||||
position: absolute;
|
||||
width: 256px;
|
||||
top: ${props => props.top}px;
|
||||
left: ${props => props.left}px;
|
||||
`;
|
||||
|
||||
export const Editor = styled.div`
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 0 rgba(9, 30, 66, 0.25);
|
||||
padding: 6px 8px 2px;
|
||||
cursor: default;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
max-width: 300px;
|
||||
min-height: 20px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export const EditorDetails = styled.div`
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
export const EditorTextarea = styled(TextareaAutosize)`
|
||||
font-family: 'Droid Sans';
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
resize: none;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
margin-bottom: 4px;
|
||||
max-height: 162px;
|
||||
min-height: 54px;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SaveButton = styled.button`
|
||||
cursor: pointer;
|
||||
background-color: #5aac44;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
margin-top: 8px;
|
||||
margin-right: 4px;
|
||||
padding: 6px 24px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
`;
|
||||
export const FadeInAnimation = keyframes`
|
||||
from { opacity: 0; transform: translateX(-20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
`;
|
||||
export const EditorButtons = styled.div`
|
||||
left: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 240px;
|
||||
z-index: 0;
|
||||
animation: ${FadeInAnimation} 85ms ease-in 1;
|
||||
`;
|
||||
|
||||
export const EditorButton = styled.div`
|
||||
cursor: pointer;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 3px;
|
||||
clear: both;
|
||||
color: #e6e6e6;
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 0 0 4px 8px;
|
||||
padding: 6px 12px 6px 8px;
|
||||
text-decoration: none;
|
||||
transition: transform 85ms ease-in;
|
||||
`;
|
||||
|
||||
export const CloseButton = styled.div`
|
||||
padding: 9px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
opacity: 0.8;
|
||||
z-index: 40;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const ListCardLabels = styled.div`
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ListCardLabel = styled.span`
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
padding: 0 8px;
|
||||
max-width: 198px;
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin: 0 4px 4px 0;
|
||||
width: auto;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
display: block;
|
||||
position: relative;
|
||||
background-color: ${props => props.color};
|
||||
`;
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import Cross from 'shared/icons/Cross';
|
||||
import {
|
||||
Wrapper,
|
||||
Container,
|
||||
Editor,
|
||||
EditorDetails,
|
||||
EditorTextarea,
|
||||
SaveButton,
|
||||
EditorButtons,
|
||||
EditorButton,
|
||||
CloseButton,
|
||||
ListCardLabels,
|
||||
ListCardLabel,
|
||||
} from './Styles';
|
||||
|
||||
type Props = {
|
||||
listId: string;
|
||||
cardId: string;
|
||||
cardTitle: string;
|
||||
onCloseEditor: () => void;
|
||||
onEditCard: (listId: string, cardId: string, cardName: string) => void;
|
||||
onOpenPopup: (popupType: number, top: number, left: number) => void;
|
||||
onArchiveCard: (listId: string, cardId: string) => void;
|
||||
labels?: Label[];
|
||||
isOpen: boolean;
|
||||
top: number;
|
||||
left: number;
|
||||
};
|
||||
|
||||
const QuickCardEditor = ({
|
||||
listId,
|
||||
cardId,
|
||||
cardTitle,
|
||||
onCloseEditor,
|
||||
onOpenPopup,
|
||||
onArchiveCard,
|
||||
onEditCard,
|
||||
labels,
|
||||
isOpen,
|
||||
top,
|
||||
left,
|
||||
}: Props) => {
|
||||
const [currentCardTitle, setCardTitle] = useState(cardTitle);
|
||||
const $editorRef: any = useRef();
|
||||
const $labelsRef: any = useRef();
|
||||
useEffect(() => {
|
||||
$editorRef.current.focus();
|
||||
$editorRef.current.select();
|
||||
}, []);
|
||||
|
||||
const handleCloseEditor = (e: any) => {
|
||||
e.stopPropagation();
|
||||
onCloseEditor();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: any) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onEditCard(listId, cardId, currentCardTitle);
|
||||
onCloseEditor();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper onClick={handleCloseEditor} open={isOpen}>
|
||||
<CloseButton onClick={handleCloseEditor}>
|
||||
<Cross size={16} color="#000" />
|
||||
</CloseButton>
|
||||
<Container left={left} top={top}>
|
||||
<Editor>
|
||||
<ListCardLabels>
|
||||
{labels &&
|
||||
labels.map(label => (
|
||||
<ListCardLabel color={label.color} key={label.name}>
|
||||
{label.name}
|
||||
</ListCardLabel>
|
||||
))}
|
||||
</ListCardLabels>
|
||||
<EditorDetails>
|
||||
<EditorTextarea
|
||||
onChange={e => setCardTitle(e.currentTarget.value)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={currentCardTitle}
|
||||
ref={$editorRef}
|
||||
/>
|
||||
</EditorDetails>
|
||||
</Editor>
|
||||
<SaveButton onClick={e => onEditCard(listId, cardId, currentCardTitle)}>Save</SaveButton>
|
||||
<EditorButtons>
|
||||
<EditorButton
|
||||
ref={$labelsRef}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
const pos = $labelsRef.current.getBoundingClientRect();
|
||||
onOpenPopup(1, pos.top + $labelsRef.current.clientHeight + 4, pos.left);
|
||||
}}
|
||||
>
|
||||
Edit Labels
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onArchiveCard(listId, cardId);
|
||||
onCloseEditor();
|
||||
}}
|
||||
>
|
||||
Archive
|
||||
</EditorButton>
|
||||
</EditorButtons>
|
||||
</Container>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickCardEditor;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import NormalizeStyles from 'App/NormalizeStyles';
|
||||
import BaseStyles from 'App/BaseStyles';
|
||||
import Sidebar from './index';
|
||||
|
||||
import Navbar from 'shared/components/Navbar';
|
||||
|
||||
export default {
|
||||
component: Sidebar,
|
||||
title: 'Sidebar',
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff' },
|
||||
{ name: 'gray', value: '#cdd3e1', default: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<Navbar />
|
||||
<Sidebar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
top: 0px;
|
||||
left: 80px;
|
||||
height: 100vh;
|
||||
width: 230px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0px 16px 24px;
|
||||
background: rgb(244, 245, 247);
|
||||
border-right: 1px solid rgb(223, 225, 230);
|
||||
`;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Container } from './Styles';
|
||||
|
||||
const Sidebar = () => {
|
||||
return <Container></Container>;
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,70 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const NavbarWrapper = styled.div`
|
||||
height: 103px;
|
||||
padding: 1.3rem 2.2rem 2.2rem;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const NavbarHeader = styled.header`
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.8rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgb(16, 22, 58);
|
||||
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
|
||||
`;
|
||||
export const Breadcrumbs = styled.div`
|
||||
color: rgb(94, 108, 132);
|
||||
font-size: 15px;
|
||||
`;
|
||||
export const BreadcrumpSeparator = styled.span`
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 18px;
|
||||
margin: 0px 10px;
|
||||
`;
|
||||
|
||||
export const ProjectActions = styled.div``;
|
||||
export const GlobalActions = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const ProfileContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const ProfileNameWrapper = styled.div`
|
||||
text-align: right;
|
||||
line-height: 1.25;
|
||||
`;
|
||||
|
||||
export const NotificationContainer = styled.div`
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
export const ProfileNamePrimary = styled.div`
|
||||
color: #c2c6dc;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const ProfileNameSecondary = styled.small`
|
||||
color: #c2c6dc;
|
||||
`;
|
||||
|
||||
export const ProfileIcon = styled.div`
|
||||
margin-left: 10px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 9999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
background: rgb(115, 103, 240);
|
||||
cursor: pointer;
|
||||
`;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { createRef, useState } from 'react';
|
||||
import NormalizeStyles from 'App/NormalizeStyles';
|
||||
import BaseStyles from 'App/BaseStyles';
|
||||
|
||||
import TopNavbar from './index';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import DropdownMenu from 'shared/components/DropdownMenu';
|
||||
|
||||
export default {
|
||||
component: TopNavbar,
|
||||
title: 'TopNavbar',
|
||||
|
||||
// Our exports that end in "Data" are not stories.
|
||||
excludeStories: /.*Data$/,
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff' },
|
||||
{ name: 'gray', value: '#f8f8f8' },
|
||||
{ name: 'darkBlue', value: '#262c49', default: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const [menu, setMenu] = useState({
|
||||
top: 0,
|
||||
left: 0,
|
||||
isOpen: false,
|
||||
});
|
||||
const onClick = (bottom: number, right: number) => {
|
||||
setMenu({
|
||||
isOpen: !menu.isOpen,
|
||||
left: right,
|
||||
top: bottom,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<TopNavbar onNotificationClick={action('notifications click')} onProfileClick={onClick} />
|
||||
{menu.isOpen && <DropdownMenu left={menu.left} top={menu.top} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Bell } from 'shared/icons';
|
||||
|
||||
import {
|
||||
NotificationContainer,
|
||||
GlobalActions,
|
||||
ProjectActions,
|
||||
NavbarWrapper,
|
||||
NavbarHeader,
|
||||
Breadcrumbs,
|
||||
BreadcrumpSeparator,
|
||||
ProfileIcon,
|
||||
ProfileContainer,
|
||||
ProfileNameWrapper,
|
||||
ProfileNamePrimary,
|
||||
ProfileNameSecondary,
|
||||
} from './Styles';
|
||||
|
||||
type NavBarProps = {
|
||||
onProfileClick: (bottom: number, right: number) => void;
|
||||
onNotificationClick: () => void;
|
||||
};
|
||||
const NavBar: React.FC<NavBarProps> = ({ onProfileClick, onNotificationClick }) => {
|
||||
const $profileRef: any = useRef(null);
|
||||
const handleProfileClick = () => {
|
||||
console.log('click');
|
||||
const boundingRect = $profileRef.current.getBoundingClientRect();
|
||||
onProfileClick(boundingRect.bottom, boundingRect.right);
|
||||
};
|
||||
return (
|
||||
<NavbarWrapper>
|
||||
<NavbarHeader>
|
||||
<ProjectActions>
|
||||
<Breadcrumbs>
|
||||
Projects
|
||||
<BreadcrumpSeparator>/</BreadcrumpSeparator>
|
||||
project name
|
||||
<BreadcrumpSeparator>/</BreadcrumpSeparator>
|
||||
Board
|
||||
</Breadcrumbs>
|
||||
</ProjectActions>
|
||||
<GlobalActions>
|
||||
<NotificationContainer onClick={onNotificationClick}>
|
||||
<Bell color="#c2c6dc" size={20} />
|
||||
</NotificationContainer>
|
||||
<ProfileContainer>
|
||||
<ProfileNameWrapper>
|
||||
<ProfileNamePrimary>Jordan Knott</ProfileNamePrimary>
|
||||
<ProfileNameSecondary>Manager</ProfileNameSecondary>
|
||||
</ProfileNameWrapper>
|
||||
<ProfileIcon ref={$profileRef} onClick={handleProfileClick}>
|
||||
JK
|
||||
</ProfileIcon>
|
||||
</ProfileContainer>
|
||||
</GlobalActions>
|
||||
</NavbarHeader>
|
||||
</NavbarWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
@@ -0,0 +1,13 @@
|
||||
const KeyCodes = {
|
||||
TAB: 9,
|
||||
ENTER: 13,
|
||||
ESCAPE: 27,
|
||||
SPACE: 32,
|
||||
ARROW_LEFT: 37,
|
||||
ARROW_UP: 38,
|
||||
ARROW_RIGHT: 39,
|
||||
ARROW_DOWN: 40,
|
||||
M: 77,
|
||||
};
|
||||
|
||||
export default KeyCodes;
|
||||
@@ -0,0 +1,14 @@
|
||||
const LabelColors = {
|
||||
GREEN: '#61bd4f',
|
||||
YELLOW: '#f2d600',
|
||||
ORANGE: '#ff9f1a',
|
||||
RED: '#eb5a46',
|
||||
PURPLE: '#c377e0',
|
||||
BLUE: '#0079bf',
|
||||
SKY: '#00c2e0',
|
||||
LIME: '#51e898',
|
||||
PINK: '#ff78cb',
|
||||
BLACK: '#344563',
|
||||
};
|
||||
|
||||
export default LabelColors;
|
||||
@@ -0,0 +1,6 @@
|
||||
const MenuTypes = {
|
||||
LABEL_MANAGER: 1,
|
||||
LABEL_EDITOR: 2,
|
||||
};
|
||||
|
||||
export default MenuTypes;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useRef } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const useDeepCompareMemoize = (value: any) => {
|
||||
const valueRef = useRef();
|
||||
|
||||
if (!isEqual(value, valueRef.current)) {
|
||||
valueRef.current = value;
|
||||
}
|
||||
return valueRef.current;
|
||||
};
|
||||
|
||||
export default useDeepCompareMemoize;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
import KeyCodes from 'shared/constants/keyCodes';
|
||||
|
||||
const useOnEscapeKeyDown = (isListening: boolean, onEscapeKeyDown: () => void) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.keyCode === KeyCodes.ESCAPE) {
|
||||
onEscapeKeyDown();
|
||||
}
|
||||
};
|
||||
if (isListening) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isListening, onEscapeKeyDown]);
|
||||
};
|
||||
export default useOnEscapeKeyDown;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
const useOnOutsideClick = (
|
||||
$ignoredElementRefs: any,
|
||||
isListening: boolean,
|
||||
onOutsideClick: () => void,
|
||||
$listeningElementRef: any,
|
||||
) => {
|
||||
const $mouseDownTargetRef = useRef();
|
||||
const $ignoredElementRefsMemoized = [$ignoredElementRefs].flat();
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseDown = (event: any) => {
|
||||
$mouseDownTargetRef.current = event.target;
|
||||
};
|
||||
|
||||
const handleMouseUp = (event: any) => {
|
||||
if (typeof $ignoredElementRefsMemoized !== 'undefined') {
|
||||
const isAnyIgnoredElementAncestorOfTarget = $ignoredElementRefsMemoized.some(
|
||||
($elementRef: any) =>
|
||||
$elementRef.current.contains($mouseDownTargetRef.current) || $elementRef.current.contains(event.target),
|
||||
);
|
||||
if (event.button === 0 && !isAnyIgnoredElementAncestorOfTarget) {
|
||||
onOutsideClick();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const $listeningElement = ($listeningElementRef || {}).current || document;
|
||||
|
||||
if (isListening) {
|
||||
$listeningElement.addEventListener('mousedown', handleMouseDown);
|
||||
$listeningElement.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
return () => {
|
||||
$listeningElement.removeEventListener('mousedown', handleMouseDown);
|
||||
$listeningElement.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [$ignoredElementRefsMemoized, $listeningElementRef, isListening, onOutsideClick]);
|
||||
};
|
||||
|
||||
export default useOnOutsideClick;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Bell = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M16.023 12.5c0-4.5-4-3.5-4-7 0-0.29-0.028-0.538-0.079-0.749-0.263-1.766-1.44-3.183-2.965-3.615 0.014-0.062 0.021-0.125 0.021-0.191 0-0.52-0.45-0.945-1-0.945s-1 0.425-1 0.945c0 0.065 0.007 0.129 0.021 0.191-1.71 0.484-2.983 2.208-3.020 4.273-0.001 0.030-0.001 0.060-0.001 0.091 0 3.5-4 2.5-4 7 0 1.191 2.665 2.187 6.234 2.439 0.336 0.631 1.001 1.061 1.766 1.061s1.43-0.43 1.766-1.061c3.568-0.251 6.234-1.248 6.234-2.439 0-0.004-0-0.007-0-0.011l0.024 0.011zM12.91 13.345c-0.847 0.226-1.846 0.389-2.918 0.479-0.089-1.022-0.947-1.824-1.992-1.824s-1.903 0.802-1.992 1.824c-1.072-0.090-2.071-0.253-2.918-0.479-1.166-0.311-1.724-0.659-1.928-0.845 0.204-0.186 0.762-0.534 1.928-0.845 1.356-0.362 3.1-0.561 4.91-0.561s3.554 0.199 4.91 0.561c1.166 0.311 1.724 0.659 1.928 0.845-0.204 0.186-0.762 0.534-1.928 0.845z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Bell.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Bell;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Checkmark = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M13.5 2l-7.5 7.5-3.5-3.5-2.5 2.5 6 6 10-10z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Checkmark.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Checkmark;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Citadel = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 12.7 12.7">
|
||||
<g transform="translate(-.26 -24.137) scale(.1249)">
|
||||
<path
|
||||
d="M50.886 286.515l-40.4-44.46 44.459-40.401 40.401 44.46z"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="11.90597031"
|
||||
/>
|
||||
<circle cx="52.917" cy="244.083" r="11.025" fill={color} />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Citadel.defaultProps = {
|
||||
size: 16,
|
||||
color: '#7367f0',
|
||||
};
|
||||
|
||||
export default Citadel;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Cross = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M15.854 12.854c-0-0-0-0-0-0l-4.854-4.854 4.854-4.854c0-0 0-0 0-0 0.052-0.052 0.090-0.113 0.114-0.178 0.066-0.178 0.028-0.386-0.114-0.529l-2.293-2.293c-0.143-0.143-0.351-0.181-0.529-0.114-0.065 0.024-0.126 0.062-0.178 0.114 0 0-0 0-0 0l-4.854 4.854-4.854-4.854c-0-0-0-0-0-0-0.052-0.052-0.113-0.090-0.178-0.114-0.178-0.066-0.386-0.029-0.529 0.114l-2.293 2.293c-0.143 0.143-0.181 0.351-0.114 0.529 0.024 0.065 0.062 0.126 0.114 0.178 0 0 0 0 0 0l4.854 4.854-4.854 4.854c-0 0-0 0-0 0-0.052 0.052-0.090 0.113-0.114 0.178-0.066 0.178-0.029 0.386 0.114 0.529l2.293 2.293c0.143 0.143 0.351 0.181 0.529 0.114 0.065-0.024 0.126-0.062 0.178-0.114 0-0 0-0 0-0l4.854-4.854 4.854 4.854c0 0 0 0 0 0 0.052 0.052 0.113 0.090 0.178 0.114 0.178 0.066 0.386 0.029 0.529-0.114l2.293-2.293c0.143-0.143 0.181-0.351 0.114-0.529-0.024-0.065-0.062-0.126-0.114-0.178z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Cross.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Cross;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Exit = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M12 10v-2h-5v-2h5v-2l3 3zM11 9v4h-5v3l-6-3v-13h11v5h-1v-4h-8l4 2v9h4v-3z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Exit.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Exit;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Home = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M16 9.226l-8-6.21-8 6.21v-2.532l8-6.21 8 6.21zM14 9v6h-4v-4h-4v4h-4v-6l6-4.5z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Home.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Lock = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M9.25 7h-0.25v-3c0-1.654-1.346-3-3-3h-2c-1.654 0-3 1.346-3 3v3h-0.25c-0.412 0-0.75 0.338-0.75 0.75v7.5c0 0.412 0.338 0.75 0.75 0.75h8.5c0.412 0 0.75-0.338 0.75-0.75v-7.5c0-0.412-0.338-0.75-0.75-0.75zM3 4c0-0.551 0.449-1 1-1h2c0.551 0 1 0.449 1 1v3h-4v-3z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Lock.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Lock;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Pencil = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill={color} width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M13.5 0c1.381 0 2.5 1.119 2.5 2.5 0 0.563-0.186 1.082-0.5 1.5l-1 1-3.5-3.5 1-1c0.418-0.314 0.937-0.5 1.5-0.5zM1 11.5l-1 4.5 4.5-1 9.25-9.25-3.5-3.5-9.25 9.25zM11.181 5.681l-7 7-0.862-0.862 7-7 0.862 0.862z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Pencil.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Pencil;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Question = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M7 11h2v2h-2zM11 4c0.552 0 1 0.448 1 1v3l-3 2h-2v-1l3-2v-1h-5v-2h6zM8 1.5c-1.736 0-3.369 0.676-4.596 1.904s-1.904 2.86-1.904 4.596c0 1.736 0.676 3.369 1.904 4.596s2.86 1.904 4.596 1.904c1.736 0 3.369-0.676 4.596-1.904s1.904-2.86 1.904-4.596c0-1.736-0.676-3.369-1.904-4.596s-2.86-1.904-4.596-1.904zM8 0v0c4.418 0 8 3.582 8 8s-3.582 8-8 8c-4.418 0-8-3.582-8-8s3.582-8 8-8z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Question.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Question;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Stack = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill={color} width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M16 5l-8-4-8 4 8 4 8-4zM8 2.328l5.345 2.672-5.345 2.672-5.345-2.672 5.345-2.672zM14.398 7.199l1.602 0.801-8 4-8-4 1.602-0.801 6.398 3.199zM14.398 10.199l1.602 0.801-8 4-8-4 1.602-0.801 6.398 3.199z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Stack.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Stack;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const User = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M9 11.041v-0.825c1.102-0.621 2-2.168 2-3.716 0-2.485 0-4.5-3-4.5s-3 2.015-3 4.5c0 1.548 0.898 3.095 2 3.716v0.825c-3.392 0.277-6 1.944-6 3.959h14c0-2.015-2.608-3.682-6-3.959z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
User.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default User;
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
size: number | string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
const Users = ({ size, color }: Props) => {
|
||||
return (
|
||||
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||
<path d="M12 12.041v-0.825c1.102-0.621 2-2.168 2-3.716 0-2.485 0-4.5-3-4.5s-3 2.015-3 4.5c0 1.548 0.898 3.095 2 3.716v0.825c-3.392 0.277-6 1.944-6 3.959h14c0-2.015-2.608-3.682-6-3.959z" />
|
||||
<path d="M5.112 12.427c0.864-0.565 1.939-0.994 3.122-1.256-0.235-0.278-0.449-0.588-0.633-0.922-0.475-0.863-0.726-1.813-0.726-2.748 0-1.344 0-2.614 0.478-3.653 0.464-1.008 1.299-1.633 2.488-1.867-0.264-1.195-0.968-1.98-2.841-1.98-3 0-3 2.015-3 4.5 0 1.548 0.898 3.095 2 3.716v0.825c-3.392 0.277-6 1.944-6 3.959h4.359c0.227-0.202 0.478-0.393 0.753-0.573z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Users.defaultProps = {
|
||||
size: 16,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
export default Users;
|
||||
@@ -0,0 +1,14 @@
|
||||
import Cross from './Cross';
|
||||
import Bell from './Bell';
|
||||
import Pencil from './Pencil';
|
||||
import Checkmark from './Checkmark';
|
||||
import User from './User';
|
||||
import Users from './Users';
|
||||
import Lock from './Lock';
|
||||
import Citadel from './Citadel';
|
||||
import Home from './Home';
|
||||
import Stack from './Stack';
|
||||
import Question from './Question';
|
||||
import Exit from './Exit';
|
||||
|
||||
export { Cross, Bell, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };
|
||||
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
const AccessAccount = ({ width, height }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
id="a9a7ffe7-bffb-40a8-a3c8-a3664a9c484c"
|
||||
data-name="Layer 1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 796 711.7711"
|
||||
>
|
||||
<title>access_account</title>
|
||||
<path
|
||||
d="M299.079,648.56106l-8.89026-35.06486a455.3229,455.3229,0,0,0-48.30717-17.33113L240.759,612.46113l-4.55175-17.95328C215.84943,588.69462,202,586.134,202,586.134s18.70738,71.13842,57.94476,125.52465l45.72014,8.031-35.51871,5.12114a184.211,184.211,0,0,0,15.888,16.83723c57.07929,52.9818,120.65488,77.29013,142.00008,54.29413s-7.623-84.58813-64.70233-137.56993c-17.69515-16.42488-39.924-29.6057-62.175-39.97928Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#e6e6e6"
|
||||
/>
|
||||
<path
|
||||
d="M383.63224,610.48142l10.51462-34.61248a455.32041,455.32041,0,0,0-32.39463-39.80627l-9.3844,13.36992L357.7514,531.711c-14.42234-15.49938-24.95448-24.85018-24.95448-24.85018s-20.75719,70.56756-15.28054,137.40647L352.50363,674.775l-33.05275-13.97575a184.2128,184.2128,0,0,0,4.89768,22.626c21.47608,74.85917,63.33463,128.5305,93.49375,119.87826s37.19806-76.3516,15.722-151.21077c-6.6578-23.20708-18.87351-45.98058-32.55921-66.36238Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#e6e6e6"
|
||||
/>
|
||||
<path
|
||||
d="M884.17981,752.05584l6.61544-7.14478a122.56157,122.56157,0,0,0-3.1639-13.4473l-3.84424,2.134,3.38713-3.65813c-1.66992-5.44865-3.12076-8.95111-3.12076-8.95111s-13.32284,14.64664-19.85509,31.47479l4.88491,11.50059-6.36018-7.27012a49.58586,49.58586,0,0,0-1.47426,6.05443c-3.60112,20.65124.22421,38.56849,8.54414,40.0193s17.98384-14.1142,21.585-34.76544a65.28076,65.28076,0,0,0-.08151-19.8969Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#e6e6e6"
|
||||
/>
|
||||
<path
|
||||
d="M982.04942,766.18571l9.35626-2.69673a122.55844,122.55844,0,0,0,4.24249-13.14691l-4.39392-.16027,4.79042-1.38071C997.43156,743.27362,998,739.52541,998,739.52541s-18.97582,5.65159-33.26621,16.68071l-1.763,12.37-1.68666-9.51114a49.58626,49.58626,0,0,0-4.39158,4.42083c-13.75737,15.817-19.74417,33.13225-13.37186,38.67479s22.69062-2.78652,36.448-18.60348a65.281,65.281,0,0,0,10.215-17.07476Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#e6e6e6"
|
||||
/>
|
||||
<path
|
||||
d="M720.49687,142.406V754.56957a48.30136,48.30136,0,0,1-48.29157,48.29169H434.31173A48.30567,48.30567,0,0,1,386,754.56957V142.406a48.30564,48.30564,0,0,1,48.31173-48.29157H463.1698a22.96636,22.96636,0,0,0,21.246,31.61713h135.6313A22.96611,22.96611,0,0,0,641.293,94.11445H672.2053A48.30134,48.30134,0,0,1,720.49687,142.406Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#3f3d56"
|
||||
/>
|
||||
<path
|
||||
d="M519.72822,347.655a23.87666,23.87666,0,0,1,11.9461-20.68789,23.89222,23.89222,0,1,0,0,41.37573A23.87652,23.87652,0,0,1,519.72822,347.655Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M549.76412,347.655a23.87668,23.87668,0,0,1,11.94609-20.68789,23.89222,23.89222,0,1,0,0,41.37573A23.87653,23.87653,0,0,1,549.76412,347.655Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#fff"
|
||||
/>
|
||||
<circle cx="377.11738" cy="253.54057" r="23.89219" fill="#6c63ff" />
|
||||
<rect x="244.57422" y="409.90405" width="213.56445" height="2" fill="#fff" />
|
||||
<circle cx="251.31877" cy="390.67147" r="6.74414" fill="#6c63ff" />
|
||||
<rect x="244.57422" y="477.34545" width="213.56445" height="2" fill="#fff" />
|
||||
<circle cx="251.31877" cy="458.1129" r="6.74414" fill="#6c63ff" />
|
||||
<path
|
||||
d="M619.0459,422.27875H479.79883a5.00588,5.00588,0,0,1-5-5V278.03168a5.00589,5.00589,0,0,1,5-5H619.0459a5.00589,5.00589,0,0,1,5,5V417.27875A5.00589,5.00589,0,0,1,619.0459,422.27875ZM479.79883,275.03168a3.00328,3.00328,0,0,0-3,3V417.27875a3.00328,3.00328,0,0,0,3,3H619.0459a3.00328,3.00328,0,0,0,3-3V278.03168a3.00328,3.00328,0,0,0-3-3Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#fff"
|
||||
/>
|
||||
<rect x="382.82955" y="522.18225" width="75.30959" height="31.47267" rx="4" fill="#6c63ff" />
|
||||
<rect x="0.79492" y="707.76733" width="795.20508" height="2" fill="#3f3d56" />
|
||||
<path
|
||||
d="M846.52086,419.196s-2.76836,17.533,0,20.30134-17.533,25.838-17.533,25.838l-16.61018-23.06969s7.38231-11.99624,4.61394-22.14691Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#ffb8b8"
|
||||
/>
|
||||
<polygon
|
||||
points="583.621 457.039 571.62 585.306 576.23 684.967 598.377 678.508 599.304 588.998 625.145 511.485 640.829 595.459 638.98 680.355 666.664 681.279 668.513 587.155 671.756 455.695 583.621 457.039"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<path
|
||||
d="M841.90692,771.70088v20.30133S837.293,806.76682,852.05759,805.844s13.84181-7.3823,13.84181-7.3823l-5.53672-24.91527Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<path
|
||||
d="M799.45869,771.70088v20.30133s4.61393,14.76461-10.15067,13.84182-13.84182-7.3823-13.84182-7.3823l5.53673-24.91527Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<circle cx="628.83347" cy="314.00805" r="21.22412" fill="#ffb8b8" />
|
||||
<path
|
||||
d="M829.91068,455.18468l1.84558-9.22788h4.61394l8.9225-12.83445,7.68768,7.29772,1.84557,43.371H804.99541l4.61394-46.13939,6.71934-4.52936s-1.18261,15.60281,11.73642,15.60281Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#575a89"
|
||||
/>
|
||||
<path
|
||||
d="M810.53214,445.034s7.64172,9.67444,19.04686,8.98977,22.47859-9.91256,22.47859-9.91256L867.745,564.07363s-17.533,1.84558-23.06969-7.3823l-42.44824-.92279.92279-111.65732Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#d0cde1"
|
||||
/>
|
||||
<path
|
||||
d="M816.762,431.5308l-40.373,19.96273,10.15067,57.21284s3.69115,21.22412,0,29.52921-7.38231,63.67235-7.38231,63.67235,41.52545,4.61394,35.98873-60.904S816.762,431.5308,816.762,431.5308Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<path
|
||||
d="M792.07638,569.61036l5.53673,5.53673s16.61018,28.60642,23.99248,18.45575-12.919-27.68363-12.919-27.68363l-9.22787-7.3823Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#ffb8b8"
|
||||
/>
|
||||
<path
|
||||
d="M845.31375,431.5308l39.9642,19.96273-13.84182,68.28629s.92279,22.14691,5.53673,34.14315,2.76836,47.06217,2.76836,47.06217-5.53673,21.22412-17.533-31.37478S845.31375,431.5308,845.31375,431.5308Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<path
|
||||
d="M876.05007,536.39H867.745s-27.68363-3.69115-25.83806,7.3823,27.68364,8.30509,27.68364,8.30509l10.15066-.92278Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#ffb8b8"
|
||||
/>
|
||||
<path
|
||||
d="M781.92572,448.72516l-6.45952,2.76837s-6.45951,13.84181-7.3823,18.45575S756.08766,529.93049,758.856,536.39s30.452,40.60266,30.452,40.60266l15.68739-16.61018L781.92572,529.0077l6.45951-39.67988Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<path
|
||||
d="M871.43613,449.648l12.20966,1.03029,2.55495.81529s25.838,70.13187,20.30133,81.20532-29.52921,31.37478-29.52921,31.37478l-6.45952-28.60642,11.99624-11.07345-11.99624-36.91151Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
<path
|
||||
d="M848.79006,391.757s5.55251-10.41917-6.663-11.36636c0,0-11.105-6.63038-19.989.94719,0,0-7.77351-1.89439-9.99452,3.78879,0,0-1.1105-2.84159,2.221-4.736,0,0-7.77352-1.8944-7.77352,7.57757,0,0-3.3315,9.472,0,17.99674s4.442,9.472,4.442,9.472-5.47461-17.87294,7.85141-18.82013,28.2399-9.12218,29.3504,1.297,3.33151,13.26075,3.33151,13.26075S861.56084,396.01938,848.79006,391.757Z"
|
||||
transform="translate(-202 -94.11445)"
|
||||
fill="#2f2e41"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export default AccessAccount;
|
||||
@@ -0,0 +1,17 @@
|
||||
let accessToken = '';
|
||||
|
||||
export function setAccessToken(newToken: string) {
|
||||
accessToken = newToken;
|
||||
}
|
||||
export function getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
export async function getNewToken() {
|
||||
return fetch('http://localhost:3333/auth/refresh_token', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
}).then(x => {
|
||||
return x.json();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export const moveItemWithinArray = (arr: any, item: any, newIndex: number) => {
|
||||
const arrClone = [...arr];
|
||||
const oldIndex = arrClone.indexOf(item);
|
||||
arrClone.splice(newIndex, 0, arrClone.splice(oldIndex, 1)[0]);
|
||||
return arrClone;
|
||||
};
|
||||
|
||||
export const insertItemIntoArray = (arr: any, item: any, index: number) => {
|
||||
const arrClone = [...arr];
|
||||
arrClone.splice(index, 0, item);
|
||||
return arrClone;
|
||||
};
|
||||
|
||||
export const updateArrayItemById = (arr: any, itemId: any, fields: any) => {
|
||||
const arrClone = [...arr];
|
||||
const item = arrClone.find(({ id }) => id === itemId);
|
||||
if (item) {
|
||||
const itemIndex = arrClone.indexOf(item);
|
||||
arrClone.splice(itemIndex, 1, { ...item, ...fields });
|
||||
}
|
||||
return arrClone;
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import { css } from 'styled-components';
|
||||
import Color from 'color';
|
||||
|
||||
export const color = {
|
||||
primary: '#0052cc', // Blue
|
||||
success: '#0B875B', // green
|
||||
danger: '#E13C3C', // red
|
||||
warning: '#F89C1C', // orange
|
||||
secondary: '#F4F5F7', // light grey
|
||||
|
||||
textDarkest: '#172b4d',
|
||||
textDark: '#42526E',
|
||||
textMedium: '#5E6C84',
|
||||
textLight: '#8993a4',
|
||||
textLink: '#0052cc',
|
||||
|
||||
backgroundDarkPrimary: '#0747A6',
|
||||
backgroundMedium: '#dfe1e6',
|
||||
backgroundLight: '#ebecf0',
|
||||
backgroundLightest: '#F4F5F7',
|
||||
backgroundLightPrimary: '#D2E5FE',
|
||||
backgroundLightSuccess: '#E4FCEF',
|
||||
|
||||
borderLightest: '#dfe1e6',
|
||||
borderLight: '#C1C7D0',
|
||||
borderInputFocus: '#4c9aff',
|
||||
};
|
||||
|
||||
export const font = {
|
||||
regular: 'font-family: "Droid Sans"; font-weight: normal;',
|
||||
size: (size: number) => `font-size: ${size}px;`,
|
||||
bold: 'font-family: "Droid Sans"; font-weight: normal;',
|
||||
medium: 'font-family: "Droid Sans"; font-weight: normal;',
|
||||
};
|
||||
|
||||
export const mixin = {
|
||||
darken: (colorValue: string, amount: number) =>
|
||||
Color(colorValue)
|
||||
.darken(amount)
|
||||
.string(),
|
||||
lighten: (colorValue: string, amount: number) =>
|
||||
Color(colorValue)
|
||||
.lighten(amount)
|
||||
.string(),
|
||||
rgba: (colorValue: string, opacity: number) =>
|
||||
Color(colorValue)
|
||||
.alpha(opacity)
|
||||
.string(),
|
||||
boxShadowCard: css`
|
||||
box-shadow: rgba(9, 30, 66, 0.25) 0px 1px 2px 0px;
|
||||
`,
|
||||
boxShadowMedium: css`
|
||||
box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.1);
|
||||
`,
|
||||
boxShadowDropdown: css`
|
||||
box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.31) 0px 0px 1px;
|
||||
`,
|
||||
truncateText: css`
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`,
|
||||
clickable: css`
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
`,
|
||||
hardwareAccelerate: css`
|
||||
transform: translateZ(0);
|
||||
`,
|
||||
cover: css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`,
|
||||
link: (colorValue = color.textLink) => css`
|
||||
cursor: pointer;
|
||||
color: ${colorValue};
|
||||
${font.medium}
|
||||
&:hover, &:visited, &:active {
|
||||
color: ${colorValue};
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`,
|
||||
|
||||
placeholderColor: (colorValue: string) => css`
|
||||
::-webkit-input-placeholder {
|
||||
color: ${colorValue} !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
:-moz-placeholder {
|
||||
color: ${colorValue} !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
::-moz-placeholder {
|
||||
color: ${colorValue} !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
:-ms-input-placeholder {
|
||||
color: ${colorValue} !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react",
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"types": [
|
||||
"react-beautiful-dnd"
|
||||
]
|
||||
}
|
||||
+15509
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user