initial commit

This commit is contained in:
Jordan Knott
2020-04-09 21:40:22 -05:00
commit 9611105364
141 changed files with 29236 additions and 0 deletions

110
web/src/App/BaseStyles.ts Normal file
View File

@ -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)}
`;

26
web/src/App/Navbar.tsx Normal file
View File

@ -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;

View File

@ -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;
}
`;

23
web/src/App/Routes.tsx Normal file
View File

@ -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;

26
web/src/App/TopNavbar.tsx Normal file
View File

@ -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;

43
web/src/App/index.tsx Normal file
View File

@ -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;

13
web/src/Auth/Styles.ts Normal file
View File

@ -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%;
`;

62
web/src/Auth/index.tsx Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

65
web/src/citadel.d.ts vendored Normal file
View File

@ -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;
};

130
web/src/index.tsx Normal file
View File

@ -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'),
);

1
web/src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

5
web/src/setupTests.ts Normal file
View File

@ -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';

View File

@ -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' }}
/>
);
};

View File

@ -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;
`;

View File

@ -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;

View File

@ -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')} />;
};

View File

@ -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;
`;

View File

@ -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;

View File

@ -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} />}
</>
);
};

View File

@ -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;
`;

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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;
`;

View File

@ -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 };

View File

@ -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}
/>
);
};

View File

@ -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;
`;

View File

@ -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;

View File

@ -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>
</>
);
};

View File

@ -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;
`;

View File

@ -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;

View File

@ -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>
</>
);
};

View File

@ -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;
}
`;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>
</>
);
};

View File

@ -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;
`;

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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);
}
`;

View File

@ -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;

View File

@ -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>
</>
);
};

View File

@ -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};
`;

View File

@ -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;

View File

@ -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 />
</>
);
};

View File

@ -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);
`;

View File

@ -0,0 +1,9 @@
import React from 'react';
import { Container } from './Styles';
const Sidebar = () => {
return <Container></Container>;
};
export default Sidebar;

View File

@ -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;
`;

View File

@ -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} />}
</>
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,6 @@
const MenuTypes = {
LABEL_MANAGER: 1,
LABEL_EDITOR: 2,
};
export default MenuTypes;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 };

View File

@ -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;

View File

@ -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();
});
}

View File

@ -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;
};

View File

@ -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;
}
`,
};