Implemented kanban board page with lists of issues
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
||||||
import { User } from 'entities';
|
|
||||||
import { catchErrors } from 'errors';
|
import { catchErrors } from 'errors';
|
||||||
import { createEntity } from 'utils/typeorm';
|
|
||||||
import { signToken } from 'utils/authToken';
|
import { signToken } from 'utils/authToken';
|
||||||
import seedGuestUserEntities from 'database/seeds/guestUser';
|
import seedGuestUserEntities from 'database/seeds/guestUser';
|
||||||
|
|
||||||
@@ -10,9 +8,8 @@ const router = express.Router();
|
|||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/authentication/guest',
|
'/authentication/guest',
|
||||||
catchErrors(async (req, res) => {
|
catchErrors(async (_req, res) => {
|
||||||
const user = await createEntity(User, req.body);
|
const user = await seedGuestUserEntities();
|
||||||
await seedGuestUserEntities(user);
|
|
||||||
res.respond({
|
res.respond({
|
||||||
authToken: signToken({ sub: user.id }),
|
authToken: signToken({ sub: user.id }),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,36 +2,20 @@ import express from 'express';
|
|||||||
|
|
||||||
import { Project } from 'entities';
|
import { Project } from 'entities';
|
||||||
import { catchErrors } from 'errors';
|
import { catchErrors } from 'errors';
|
||||||
import { findEntityOrThrow, updateEntity, deleteEntity, createEntity } from 'utils/typeorm';
|
import { findEntityOrThrow, updateEntity } from 'utils/typeorm';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/projects',
|
'/project',
|
||||||
catchErrors(async (_req, res) => {
|
|
||||||
const projects = await Project.find();
|
|
||||||
res.respond({ projects });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/projects/:projectId',
|
|
||||||
catchErrors(async (req, res) => {
|
catchErrors(async (req, res) => {
|
||||||
const project = await findEntityOrThrow(Project, req.params.projectId, {
|
const project = await findEntityOrThrow(Project, req.currentUser.projectId, {
|
||||||
relations: ['users', 'issues', 'issues.comments'],
|
relations: ['users', 'issues', 'issues.comments'],
|
||||||
});
|
});
|
||||||
res.respond({ project });
|
res.respond({ project });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/projects',
|
|
||||||
catchErrors(async (req, res) => {
|
|
||||||
const project = await createEntity(Project, req.body);
|
|
||||||
res.respond({ project });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.put(
|
router.put(
|
||||||
'/projects/:projectId',
|
'/projects/:projectId',
|
||||||
catchErrors(async (req, res) => {
|
catchErrors(async (req, res) => {
|
||||||
@@ -40,12 +24,4 @@ router.put(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
router.delete(
|
|
||||||
'/projects/:projectId',
|
|
||||||
catchErrors(async (req, res) => {
|
|
||||||
const project = await deleteEntity(Project, req.params.projectId);
|
|
||||||
res.respond({ project });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
14
api/src/controllers/users.ts
Normal file
14
api/src/controllers/users.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { catchErrors } from 'errors';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/currentUser',
|
||||||
|
catchErrors((req, res) => {
|
||||||
|
res.respond({ currentUser: req.currentUser });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -4,6 +4,7 @@ import User from 'entities/User';
|
|||||||
|
|
||||||
const generateUser = (data: Partial<User> = {}): Partial<User> => ({
|
const generateUser = (data: Partial<User> = {}): Partial<User> => ({
|
||||||
name: faker.company.companyName(),
|
name: faker.company.companyName(),
|
||||||
|
avatarUrl: faker.image.avatar(),
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
...data,
|
...data,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,39 @@
|
|||||||
|
import faker from 'faker';
|
||||||
|
import { sample } from 'lodash';
|
||||||
|
|
||||||
import { Comment, Issue, Project, User } from 'entities';
|
import { Comment, Issue, Project, User } from 'entities';
|
||||||
import { ProjectCategory } from 'constants/projects';
|
import { ProjectCategory } from 'constants/projects';
|
||||||
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
|
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
|
||||||
import { createEntity } from 'utils/typeorm';
|
import { createEntity } from 'utils/typeorm';
|
||||||
|
|
||||||
const seedProject = (user: User): Promise<Project> =>
|
const seedUsers = (): Promise<User[]> => {
|
||||||
|
const users = [
|
||||||
|
createEntity(User, {
|
||||||
|
email: 'greg@jira.guest',
|
||||||
|
name: 'Greg the Egg',
|
||||||
|
avatarUrl: faker.image.avatar(),
|
||||||
|
}),
|
||||||
|
createEntity(User, {
|
||||||
|
email: 'yoda@jira.guest',
|
||||||
|
name: 'Baby Yoda',
|
||||||
|
avatarUrl: faker.image.avatar(),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
return Promise.all(users);
|
||||||
|
};
|
||||||
|
|
||||||
|
const seedProject = (users: User[]): Promise<Project> =>
|
||||||
createEntity(Project, {
|
createEntity(Project, {
|
||||||
name: 'Project: Hello World',
|
name: 'singularity 1.0',
|
||||||
url: 'https://www.atlassian.com/software/jira',
|
url: 'https://www.atlassian.com/software/jira',
|
||||||
description:
|
description:
|
||||||
'Plan, track, and manage your agile and software development projects in Jira. Customize your workflow, collaborate, and release great software.',
|
'Plan, track, and manage your agile and software development projects in Jira. Customize your workflow, collaborate, and release great software.',
|
||||||
category: ProjectCategory.SOFTWARE,
|
category: ProjectCategory.SOFTWARE,
|
||||||
users: [user],
|
users,
|
||||||
});
|
});
|
||||||
|
|
||||||
const seedIssues = (project: Project): Promise<Issue[]> => {
|
const seedIssues = (project: Project): Promise<Issue[]> => {
|
||||||
const user = project.users[0];
|
const getRandomUser = (): User => sample(project.users) as User;
|
||||||
const issues = [
|
const issues = [
|
||||||
createEntity(Issue, {
|
createEntity(Issue, {
|
||||||
title: 'This is an issue of type: Task.',
|
title: 'This is an issue of type: Task.',
|
||||||
@@ -22,9 +41,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
status: IssueStatus.BACKLOG,
|
status: IssueStatus.BACKLOG,
|
||||||
priority: IssuePriority.LOWEST,
|
priority: IssuePriority.LOWEST,
|
||||||
estimate: 8,
|
estimate: 8,
|
||||||
reporterId: user.id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
users: [user],
|
users: [getRandomUser()],
|
||||||
}),
|
}),
|
||||||
createEntity(Issue, {
|
createEntity(Issue, {
|
||||||
title: "Click on an issue to see what's behind it.",
|
title: "Click on an issue to see what's behind it.",
|
||||||
@@ -33,7 +52,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
priority: IssuePriority.LOW,
|
priority: IssuePriority.LOW,
|
||||||
description: 'Nothing in particular.',
|
description: 'Nothing in particular.',
|
||||||
estimate: 40,
|
estimate: 40,
|
||||||
reporterId: user.id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
}),
|
}),
|
||||||
createEntity(Issue, {
|
createEntity(Issue, {
|
||||||
@@ -42,9 +61,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
status: IssueStatus.BACKLOG,
|
status: IssueStatus.BACKLOG,
|
||||||
priority: IssuePriority.MEDIUM,
|
priority: IssuePriority.MEDIUM,
|
||||||
estimate: 15,
|
estimate: 15,
|
||||||
reporterId: user.id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
users: [user],
|
users: [getRandomUser()],
|
||||||
}),
|
}),
|
||||||
createEntity(Issue, {
|
createEntity(Issue, {
|
||||||
title: 'You can use markdown for issue descriptions.',
|
title: 'You can use markdown for issue descriptions.',
|
||||||
@@ -54,9 +73,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
description:
|
description:
|
||||||
"#### Colons can be used to align columns.\n\n| Tables | Are | Cool |\n| ------------- |:-------------:| -----:|\n| col 3 is | right-aligned | |\n| col 2 is | centered | |\n| zebra stripes | are neat | |\n\nThe outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.\n\nMarkdown | Less | Pretty\n--- | --- | ---\n*Still* | `renders` | **nicely**\n1 | 2 | 3",
|
"#### Colons can be used to align columns.\n\n| Tables | Are | Cool |\n| ------------- |:-------------:| -----:|\n| col 3 is | right-aligned | |\n| col 2 is | centered | |\n| zebra stripes | are neat | |\n\nThe outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.\n\nMarkdown | Less | Pretty\n--- | --- | ---\n*Still* | `renders` | **nicely**\n1 | 2 | 3",
|
||||||
estimate: 4,
|
estimate: 4,
|
||||||
reporterId: user.id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
users: [user],
|
users: [getRandomUser()],
|
||||||
}),
|
}),
|
||||||
createEntity(Issue, {
|
createEntity(Issue, {
|
||||||
title: 'You must assign priority from lowest to highest to all issues.',
|
title: 'You must assign priority from lowest to highest to all issues.',
|
||||||
@@ -64,7 +83,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
status: IssueStatus.SELECTED,
|
status: IssueStatus.SELECTED,
|
||||||
priority: IssuePriority.HIGHEST,
|
priority: IssuePriority.HIGHEST,
|
||||||
estimate: 15,
|
estimate: 15,
|
||||||
reporterId: user.id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
}),
|
}),
|
||||||
createEntity(Issue, {
|
createEntity(Issue, {
|
||||||
@@ -73,9 +92,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
status: IssueStatus.SELECTED,
|
status: IssueStatus.SELECTED,
|
||||||
priority: IssuePriority.MEDIUM,
|
priority: IssuePriority.MEDIUM,
|
||||||
estimate: 55,
|
estimate: 55,
|
||||||
reporterId: user.id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
users: [user],
|
users: [getRandomUser()],
|
||||||
}),
|
}),
|
||||||
createEntity(Issue, {
|
createEntity(Issue, {
|
||||||
title: 'Try leaving a comment on this issue.',
|
title: 'Try leaving a comment on this issue.',
|
||||||
@@ -83,7 +102,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
status: IssueStatus.SELECTED,
|
status: IssueStatus.SELECTED,
|
||||||
priority: IssuePriority.MEDIUM,
|
priority: IssuePriority.MEDIUM,
|
||||||
estimate: 12,
|
estimate: 12,
|
||||||
reporterId: user.id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -97,10 +116,12 @@ const seedComments = (issue: Issue, user: User): Promise<Comment> =>
|
|||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
|
|
||||||
const seedGuestUserEntities = async (user: User): Promise<void> => {
|
const seedGuestUserEntities = async (): Promise<User> => {
|
||||||
const project = await seedProject(user);
|
const users = await seedUsers();
|
||||||
|
const project = await seedProject(users);
|
||||||
const issues = await seedIssues(project);
|
const issues = await seedIssues(project);
|
||||||
await seedComments(issues[issues.length - 1], project.users[0]);
|
await seedComments(issues[issues.length - 1], project.users[0]);
|
||||||
|
return users[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default seedGuestUserEntities;
|
export default seedGuestUserEntities;
|
||||||
|
|||||||
@@ -41,16 +41,16 @@ class Issue extends BaseEntity {
|
|||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
priority: IssuePriority;
|
priority: IssuePriority;
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column('text', { nullable: true })
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
@Column('integer', { nullable: true })
|
||||||
estimate: number | null;
|
estimate: number | null;
|
||||||
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
@Column('integer', { nullable: true })
|
||||||
timeSpent: number | null;
|
timeSpent: number | null;
|
||||||
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
@Column('integer', { nullable: true })
|
||||||
timeRemaining: number | null;
|
timeRemaining: number | null;
|
||||||
|
|
||||||
@CreateDateColumn({ type: 'timestamp' })
|
@CreateDateColumn({ type: 'timestamp' })
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
ManyToMany,
|
|
||||||
JoinTable,
|
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import is from 'utils/validation';
|
import is from 'utils/validation';
|
||||||
@@ -32,7 +30,7 @@ class Project extends BaseEntity {
|
|||||||
@Column('varchar', { nullable: true })
|
@Column('varchar', { nullable: true })
|
||||||
url: string | null;
|
url: string | null;
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column('text', { nullable: true })
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
|
||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
@@ -50,11 +48,10 @@ class Project extends BaseEntity {
|
|||||||
)
|
)
|
||||||
issues: Issue[];
|
issues: Issue[];
|
||||||
|
|
||||||
@ManyToMany(
|
@OneToMany(
|
||||||
() => User,
|
() => User,
|
||||||
user => user.projects,
|
user => user.project,
|
||||||
)
|
)
|
||||||
@JoinTable()
|
|
||||||
users: User[];
|
users: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
|
ManyToOne,
|
||||||
|
RelationId,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import is from 'utils/validation';
|
import is from 'utils/validation';
|
||||||
@@ -28,6 +30,9 @@ class User extends BaseEntity {
|
|||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
|
@Column('varchar', { length: 2000 })
|
||||||
|
avatarUrl: string;
|
||||||
|
|
||||||
@CreateDateColumn({ type: 'timestamp' })
|
@CreateDateColumn({ type: 'timestamp' })
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
@@ -46,11 +51,14 @@ class User extends BaseEntity {
|
|||||||
)
|
)
|
||||||
issues: Issue[];
|
issues: Issue[];
|
||||||
|
|
||||||
@ManyToMany(
|
@ManyToOne(
|
||||||
() => Project,
|
() => Project,
|
||||||
project => project.users,
|
project => project.users,
|
||||||
)
|
)
|
||||||
projects: Project[];
|
project: Project;
|
||||||
|
|
||||||
|
@RelationId((user: User) => user.project)
|
||||||
|
projectId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
|||||||
@@ -17,5 +17,5 @@ export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
|
|||||||
data: {},
|
data: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
res.status(errorData.status).send({ errors: [errorData] });
|
res.status(errorData.status).send({ error: errorData });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { authenticateUser } from 'middleware/authentication';
|
|||||||
import authenticationRoutes from 'controllers/authentication';
|
import authenticationRoutes from 'controllers/authentication';
|
||||||
import projectsRoutes from 'controllers/projects';
|
import projectsRoutes from 'controllers/projects';
|
||||||
import issuesRoutes from 'controllers/issues';
|
import issuesRoutes from 'controllers/issues';
|
||||||
|
import usersRoutes from 'controllers/users';
|
||||||
import { RouteNotFoundError } from 'errors';
|
import { RouteNotFoundError } from 'errors';
|
||||||
import { errorHandler } from 'errors/errorHandler';
|
import { errorHandler } from 'errors/errorHandler';
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ const initializeExpress = (): void => {
|
|||||||
|
|
||||||
app.use((_req, res, next) => {
|
app.use((_req, res, next) => {
|
||||||
res.respond = (data): void => {
|
res.respond = (data): void => {
|
||||||
res.status(200).send({ data });
|
res.status(200).send(data);
|
||||||
};
|
};
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
@@ -41,6 +42,7 @@ const initializeExpress = (): void => {
|
|||||||
|
|
||||||
app.use('/', projectsRoutes);
|
app.use('/', projectsRoutes);
|
||||||
app.use('/', issuesRoutes);
|
app.use('/', issuesRoutes);
|
||||||
|
app.use('/', usersRoutes);
|
||||||
|
|
||||||
app.use((req, _res, next) => next(new RouteNotFoundError(req.originalUrl)));
|
app.use((req, _res, next) => next(new RouteNotFoundError(req.originalUrl)));
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
|
||||||
import { verifyToken } from 'utils/authToken';
|
import { verifyToken } from 'utils/authToken';
|
||||||
import { findEntityOrThrow } from 'utils/typeorm';
|
|
||||||
import { catchErrors, InvalidTokenError } from 'errors';
|
import { catchErrors, InvalidTokenError } from 'errors';
|
||||||
import { User } from 'entities';
|
import { User } from 'entities';
|
||||||
|
|
||||||
@@ -20,6 +19,10 @@ export const authenticateUser = catchErrors(async (req, _res, next) => {
|
|||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new InvalidTokenError('Authentication token is invalid.');
|
throw new InvalidTokenError('Authentication token is invalid.');
|
||||||
}
|
}
|
||||||
req.currentUser = await findEntityOrThrow(User, userId);
|
const user = await User.findOne(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new InvalidTokenError('Authentication token is invalid: User not found.');
|
||||||
|
}
|
||||||
|
req.currentUser = user;
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { InvalidTokenError } from 'errors';
|
|||||||
|
|
||||||
export const signToken = (payload: object, options?: SignOptions): string =>
|
export const signToken = (payload: object, options?: SignOptions): string =>
|
||||||
jwt.sign(payload, process.env.JWT_SECRET, {
|
jwt.sign(payload, process.env.JWT_SECRET, {
|
||||||
expiresIn: '7 days',
|
expiresIn: '180 days',
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Toast from './Toast';
|
|
||||||
import Routes from './Routes';
|
|
||||||
import NormalizeStyles from './NormalizeStyles';
|
import NormalizeStyles from './NormalizeStyles';
|
||||||
import FontStyles from './FontStyles';
|
import FontStyles from './FontStyles';
|
||||||
import BaseStyles from './BaseStyles';
|
import BaseStyles from './BaseStyles';
|
||||||
|
import Toast from './Toast';
|
||||||
|
import Routes from './Routes';
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { sizes } from 'shared/utils/styles';
|
||||||
|
|
||||||
export const Main = styled.main`
|
export const Main = styled.main`
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: 75px;
|
padding-left: ${sizes.appNavBarLeftWidth}px;
|
||||||
`;
|
`;
|
||||||
|
|||||||
29
client/src/components/App/Authenticate.jsx
Normal file
29
client/src/components/App/Authenticate.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import useApi from 'shared/hooks/api';
|
||||||
|
import { getStoredAuthToken, storeAuthToken } from 'shared/utils/authToken';
|
||||||
|
import { PageLoader } from 'shared/components';
|
||||||
|
|
||||||
|
const Authenticate = () => {
|
||||||
|
const [{ data }, createGuestAccount] = useApi.post('/authentication/guest');
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!getStoredAuthToken()) {
|
||||||
|
createGuestAccount();
|
||||||
|
}
|
||||||
|
}, [createGuestAccount]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && data.authToken) {
|
||||||
|
storeAuthToken(data.authToken);
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
}, [data, history]);
|
||||||
|
|
||||||
|
return <PageLoader />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Authenticate;
|
||||||
@@ -85,7 +85,7 @@ export default createGlobalStyle`
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
line-height: 1.6;
|
line-height: 1.4285;
|
||||||
a {
|
a {
|
||||||
${mixin.link()}
|
${mixin.link()}
|
||||||
}
|
}
|
||||||
@@ -104,5 +104,5 @@ export default createGlobalStyle`
|
|||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
${mixin.placeholderColor(color.textLightBlue)}
|
${mixin.placeholderColor(color.textLight)}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import Logo from 'shared/components/Logo';
|
|||||||
|
|
||||||
export const NavLeft = styled.aside`
|
export const NavLeft = styled.aside`
|
||||||
z-index: ${zIndexValues.navLeft};
|
z-index: ${zIndexValues.navLeft};
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
width: ${sizes.appNavBarLeftWidth}px;
|
width: ${sizes.appNavBarLeftWidth}px;
|
||||||
background: ${color.primary};
|
background: ${color.backgroundDarkPrimary};
|
||||||
transition: all 0.1s;
|
transition: all 0.1s;
|
||||||
${mixin.hardwareAccelerate}
|
${mixin.hardwareAccelerate}
|
||||||
&:hover {
|
&:hover {
|
||||||
width: 260px;
|
width: 180px;
|
||||||
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.6);
|
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -25,71 +25,43 @@ export const LogoLink = styled(NavLink)`
|
|||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 0;
|
left: 0;
|
||||||
margin: 40px 0 40px;
|
margin: 20px 0 10px;
|
||||||
transition: left 0.1s;
|
transition: left 0.1s;
|
||||||
&:before {
|
|
||||||
display: inline-block;
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 50px;
|
|
||||||
width: 20px;
|
|
||||||
background: ${color.primary};
|
|
||||||
}
|
|
||||||
${NavLeft}:hover & {
|
|
||||||
left: 3px;
|
|
||||||
&:before {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledLogo = styled(Logo)`
|
export const StyledLogo = styled(Logo)`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 13px;
|
margin-left: 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
${mixin.clickable}
|
${mixin.clickable}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const IconLink = styled(NavLink)`
|
export const Bottom = styled.div`
|
||||||
display: block;
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Item = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px;
|
height: 42px;
|
||||||
line-height: 60px;
|
line-height: 42px;
|
||||||
padding-left: 67px;
|
padding-left: 67px;
|
||||||
color: rgba(255, 255, 255, 0.75);
|
color: #deebff;
|
||||||
transition: color 0.1s;
|
transition: color 0.1s;
|
||||||
${mixin.clickable}
|
${mixin.clickable}
|
||||||
&:before {
|
|
||||||
content: '';
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
right: 0;
|
|
||||||
height: 50px;
|
|
||||||
width: 5px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 6px 0 0 6px;
|
|
||||||
}
|
|
||||||
&.active,
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
&.active:before {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
i {
|
i {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 27px;
|
left: 18px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LinkText = styled.div`
|
export const ItemText = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Icon } from 'shared/components';
|
import { Icon } from 'shared/components';
|
||||||
import { NavLeft, LogoLink, StyledLogo, IconLink, LinkText } from './Styles';
|
import { NavLeft, LogoLink, StyledLogo, Bottom, Item, ItemText } from './Styles';
|
||||||
|
|
||||||
const NavbarLeft = () => (
|
const NavbarLeft = () => (
|
||||||
<NavLeft>
|
<NavLeft>
|
||||||
<LogoLink to="/">
|
<LogoLink to="/">
|
||||||
<StyledLogo color="#fff" />
|
<StyledLogo color="#fff" />
|
||||||
</LogoLink>
|
</LogoLink>
|
||||||
<IconLink to="/projects">
|
<Item>
|
||||||
<Icon type="archive" size={16} />
|
<Icon type="search" size={22} top={1} left={3} />
|
||||||
<LinkText>Projects</LinkText>
|
<ItemText>Search</ItemText>
|
||||||
</IconLink>
|
</Item>
|
||||||
<IconLink to="/subcontractors">
|
<Item>
|
||||||
<Icon type="briefcase" size={16} />
|
<Icon type="plus" size={27} />
|
||||||
<LinkText>Subcontractors</LinkText>
|
<ItemText>Create</ItemText>
|
||||||
</IconLink>
|
</Item>
|
||||||
<IconLink to="/bids">
|
<Bottom>
|
||||||
<Icon type="file-text" size={20} left={-2} />
|
<Item>
|
||||||
<LinkText>Bids</LinkText>
|
<Icon type="help" size={25} />
|
||||||
</IconLink>
|
<ItemText>Help</ItemText>
|
||||||
|
</Item>
|
||||||
|
</Bottom>
|
||||||
</NavLeft>
|
</NavLeft>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Router, Switch, Route, Redirect } from 'react-router-dom';
|
||||||
import history from 'browserHistory';
|
import history from 'browserHistory';
|
||||||
import { Router, Switch, Route } from 'react-router-dom';
|
|
||||||
|
|
||||||
import PageNotFound from 'components/PageNotFound';
|
import PageError from 'shared/components/PageError';
|
||||||
|
|
||||||
|
import Project from 'components/Project';
|
||||||
|
|
||||||
import NavbarLeft from './NavbarLeft';
|
import NavbarLeft from './NavbarLeft';
|
||||||
|
import Authenticate from './Authenticate';
|
||||||
import { Main } from './AppStyles';
|
import { Main } from './AppStyles';
|
||||||
|
|
||||||
const Routes = () => (
|
const Routes = () => (
|
||||||
@@ -12,7 +15,10 @@ const Routes = () => (
|
|||||||
<Main>
|
<Main>
|
||||||
<NavbarLeft />
|
<NavbarLeft />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route component={PageNotFound} />
|
<Redirect exact from="/" to="/project" />
|
||||||
|
<Route path="/authenticate" component={Authenticate} />
|
||||||
|
<Route path="/project" component={Project} />
|
||||||
|
<Route component={PageError} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Main>
|
</Main>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const StyledToast = styled.div`
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: ${props => color[props.type]};
|
background: ${props => color[props.type]};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||||
import pubsub from 'sweet-pubsub';
|
import pubsub from 'sweet-pubsub';
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash';
|
||||||
@@ -6,57 +6,44 @@ import { uniqueId } from 'lodash';
|
|||||||
import { Icon } from 'shared/components';
|
import { Icon } from 'shared/components';
|
||||||
import { Container, StyledToast, Title, Message } from './Styles';
|
import { Container, StyledToast, Title, Message } from './Styles';
|
||||||
|
|
||||||
class Toast extends Component {
|
const Toast = () => {
|
||||||
state = { toasts: [] };
|
const [toasts, setToasts] = useState([]);
|
||||||
|
|
||||||
componentDidMount() {
|
useEffect(() => {
|
||||||
pubsub.on('toast', this.addToast);
|
const addToast = ({ type = 'success', title, message, duration = 5 }) => {
|
||||||
}
|
const id = uniqueId();
|
||||||
|
|
||||||
componentWillUnmount() {
|
setToasts(currentToasts => [...currentToasts, { id, type, title, message }]);
|
||||||
pubsub.off('toast', this.addToast);
|
|
||||||
}
|
|
||||||
|
|
||||||
addToast = ({ type = 'success', title, message, duration = 5 }) => {
|
if (duration) {
|
||||||
const id = uniqueId('toast-');
|
setTimeout(() => removeToast(id), duration * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pubsub.on('toast', addToast);
|
||||||
|
return () => {
|
||||||
|
pubsub.off('toast', addToast);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
this.setState(state => ({
|
const removeToast = id => {
|
||||||
toasts: [...state.toasts, { id, type, title, message }],
|
setToasts(currentToasts => currentToasts.filter(toast => toast.id !== id));
|
||||||
}));
|
|
||||||
|
|
||||||
if (duration) {
|
|
||||||
setTimeout(() => this.removeToast(id), duration * 1000);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
removeToast = id => {
|
return (
|
||||||
this.setState(state => ({
|
<Container>
|
||||||
toasts: state.toasts.filter(toast => toast.id !== id),
|
<TransitionGroup>
|
||||||
}));
|
{toasts.map(toast => (
|
||||||
};
|
<CSSTransition key={toast.id} classNames="jira-toast" timeout={200}>
|
||||||
|
<StyledToast key={toast.id} type={toast.type} onClick={() => removeToast(toast.id)}>
|
||||||
render() {
|
<Icon type="close" />
|
||||||
const { toasts } = this.state;
|
{toast.title && <Title>{toast.title}</Title>}
|
||||||
return (
|
{toast.message && <Message>{toast.message}</Message>}
|
||||||
<Container>
|
</StyledToast>
|
||||||
<TransitionGroup>
|
</CSSTransition>
|
||||||
{toasts.map(toast => (
|
))}
|
||||||
<CSSTransition key={toast.id} classNames="jira-toast" timeout={200}>
|
</TransitionGroup>
|
||||||
<StyledToast
|
</Container>
|
||||||
key={toast.id}
|
);
|
||||||
type={toast.type}
|
};
|
||||||
onClick={() => this.removeToast(toast.id)}
|
|
||||||
>
|
|
||||||
<Icon type="close" />
|
|
||||||
{toast.title && <Title>{toast.title}</Title>}
|
|
||||||
{toast.message && <Message>{toast.message}</Message>}
|
|
||||||
</StyledToast>
|
|
||||||
</CSSTransition>
|
|
||||||
))}
|
|
||||||
</TransitionGroup>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Toast;
|
export default Toast;
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { color, font } from 'shared/utils/styles';
|
|
||||||
|
|
||||||
export const Wrapper = styled.div`
|
|
||||||
margin: 50px auto 0;
|
|
||||||
max-width: 500px;
|
|
||||||
padding: 50px 50px 60px;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: ${color.backgroundLight};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Heading = styled.h1`
|
|
||||||
${font.size(60)}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Message = styled.p`
|
|
||||||
color: ${color.textDark};
|
|
||||||
padding: 10px 0 30px;
|
|
||||||
line-height: 1.35;
|
|
||||||
${font.size(20)}
|
|
||||||
`;
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ConfirmModal,
|
|
||||||
Avatar,
|
|
||||||
DatePicker,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
Select,
|
|
||||||
Textarea,
|
|
||||||
Spinner,
|
|
||||||
} from 'shared/components';
|
|
||||||
import { Wrapper, Heading, Message } from './Styles';
|
|
||||||
|
|
||||||
const PageNotFound = () => {
|
|
||||||
const [dateValue, setDateValue] = useState(null);
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
|
||||||
const [isModalOpen, setModalOpen] = useState(false);
|
|
||||||
const [selectValue, setSelectValue] = useState('');
|
|
||||||
const [selectOptions, setSelectOptions] = useState([
|
|
||||||
{ label: 'one', value: '1' },
|
|
||||||
{ label: 'two', value: '2' },
|
|
||||||
{ label: 'three', value: '3' },
|
|
||||||
{ label: 'four', value: '4' },
|
|
||||||
{ label: 'five', value: '5' },
|
|
||||||
{ label: 'six', value: '6' },
|
|
||||||
{ label: 'seven', value: '7' },
|
|
||||||
{ label: 'eight', value: '8' },
|
|
||||||
{ label: 'nine', value: '9' },
|
|
||||||
{ label: 'ten', value: '10' },
|
|
||||||
]);
|
|
||||||
console.log('ha');
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<Heading>404</Heading>
|
|
||||||
<Message>We cannot find the page you are looking for.</Message>
|
|
||||||
<div style={{ textAlign: 'left' }}>
|
|
||||||
<Avatar name="Ivor Reic" size={40} />
|
|
||||||
<ConfirmModal
|
|
||||||
renderLink={modal => <Button onClick={modal.open}>Yo</Button>}
|
|
||||||
confirmInput="YAY"
|
|
||||||
onConfirm={modal => {
|
|
||||||
console.log('CONFIRMED!');
|
|
||||||
modal.close();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DatePicker placeholder="Select date" value={dateValue} onChange={setDateValue} />
|
|
||||||
<Input
|
|
||||||
placeholder="Write anything mon"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(event, value) => setInputValue(value)}
|
|
||||||
/>
|
|
||||||
<Textarea
|
|
||||||
placeholder="Write anything mon"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(event, value) => setInputValue(value)}
|
|
||||||
/>
|
|
||||||
<Button onClick={() => setModalOpen(true)}>OPEN MODAL CONTROLLED</Button>
|
|
||||||
<Modal
|
|
||||||
// renderLink={modal => <Button onClick={modal.open}>OPEN MODAL</Button>}
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
onClose={() => setModalOpen(false)}
|
|
||||||
renderContent={modal => (
|
|
||||||
<>
|
|
||||||
<h1>Nice modal bro</h1>
|
|
||||||
<h1>Nice modal bro</h1>
|
|
||||||
<Button onClick={modal.close}>Close</Button>
|
|
||||||
<Modal
|
|
||||||
renderLink={innerModal => <Button onClick={innerModal.open}>Open Modal</Button>}
|
|
||||||
renderContent={innerModal => (
|
|
||||||
<>
|
|
||||||
<h1>Nice innerModal bro</h1>
|
|
||||||
<Button onClick={innerModal.close}>Close</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
isMulti
|
|
||||||
value={selectValue}
|
|
||||||
onChange={setSelectValue}
|
|
||||||
placeholder="Type to search"
|
|
||||||
onCreate={(newOptionName, selectOptionValue) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setSelectOptions([...selectOptions, { label: newOptionName, value: newOptionName }]);
|
|
||||||
selectOptionValue(newOptionName);
|
|
||||||
}, 1000);
|
|
||||||
}}
|
|
||||||
options={selectOptions}
|
|
||||||
/>
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
<Link to="/">
|
|
||||||
<Button>Home</Button>
|
|
||||||
</Link>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageNotFound;
|
|
||||||
55
client/src/components/Project/Board/Filters/Styles.js
Normal file
55
client/src/components/Project/Board/Filters/Styles.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { Input, Avatar, Button } from 'shared/components';
|
||||||
|
import { color, font, mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const Filters = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SearchInput = styled(Input)`
|
||||||
|
margin-right: 18px;
|
||||||
|
width: 160px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Avatars = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
margin: 0 12px 0 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AvatarIsActiveBorder = styled.div`
|
||||||
|
display: inline-flex;
|
||||||
|
margin-left: -2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
${mixin.clickable};
|
||||||
|
${props => props.isActive && `box-shadow: 0 0 0 4px ${color.primary}`}
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledAvatar = styled(Avatar)`
|
||||||
|
box-shadow: 0 0 0 2px #fff;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledButton = styled(Button)`
|
||||||
|
margin-left: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ClearAll = styled.div`
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
margin-left: 15px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 1px solid ${color.borderLightest};
|
||||||
|
color: ${color.textDark};
|
||||||
|
${font.size(14.5)}
|
||||||
|
${mixin.clickable}
|
||||||
|
&:hover {
|
||||||
|
color: ${color.textMedium};
|
||||||
|
}
|
||||||
|
`;
|
||||||
91
client/src/components/Project/Board/Filters/index.jsx
Normal file
91
client/src/components/Project/Board/Filters/index.jsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { intersection, xor } from 'lodash';
|
||||||
|
|
||||||
|
import useDebounceValue from 'shared/hooks/debounceValue';
|
||||||
|
import {
|
||||||
|
Filters,
|
||||||
|
SearchInput,
|
||||||
|
Avatars,
|
||||||
|
AvatarIsActiveBorder,
|
||||||
|
StyledAvatar,
|
||||||
|
StyledButton,
|
||||||
|
ClearAll,
|
||||||
|
} from './Styles';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
project: PropTypes.object.isRequired,
|
||||||
|
currentUser: PropTypes.object.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectBoardFilters = ({ project, currentUser, onChange }) => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [userIds, setUserIds] = useState([]);
|
||||||
|
const [myOnly, setMyOnly] = useState(false);
|
||||||
|
const [recent, setRecent] = useState(false);
|
||||||
|
const debouncedSearchQuery = useDebounceValue(searchQuery, 500);
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setSearchQuery('');
|
||||||
|
setUserIds([]);
|
||||||
|
setMyOnly(false);
|
||||||
|
setRecent(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const areFiltersCleared = !searchQuery && userIds.length === 0 && !myOnly && !recent;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getFilteredIssues = () => {
|
||||||
|
let { issues } = project;
|
||||||
|
|
||||||
|
if (debouncedSearchQuery) {
|
||||||
|
issues = issues.filter(issue =>
|
||||||
|
issue.title.toLowerCase().includes(debouncedSearchQuery.toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (userIds.length > 0) {
|
||||||
|
issues = issues.filter(issue => intersection(issue.userIds, userIds).length > 0);
|
||||||
|
}
|
||||||
|
if (myOnly) {
|
||||||
|
issues = issues.filter(issue => issue.userIds.includes(currentUser.id));
|
||||||
|
}
|
||||||
|
if (recent) {
|
||||||
|
issues = issues.filter(issue =>
|
||||||
|
moment(issue.updatedAt).isAfter(moment().subtract(3, 'days')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return issues;
|
||||||
|
};
|
||||||
|
onChange(getFilteredIssues());
|
||||||
|
}, [project, currentUser, onChange, debouncedSearchQuery, userIds, myOnly, recent]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Filters>
|
||||||
|
<SearchInput icon="search" value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
<Avatars>
|
||||||
|
{project.users.map(user => (
|
||||||
|
<AvatarIsActiveBorder key={user.id} isActive={userIds.includes(user.id)}>
|
||||||
|
<StyledAvatar
|
||||||
|
avatarUrl={user.avatarUrl}
|
||||||
|
name={user.name}
|
||||||
|
onClick={() => setUserIds(value => xor(value, [user.id]))}
|
||||||
|
/>
|
||||||
|
</AvatarIsActiveBorder>
|
||||||
|
))}
|
||||||
|
</Avatars>
|
||||||
|
<StyledButton color="empty" isActive={myOnly} onClick={() => setMyOnly(!myOnly)}>
|
||||||
|
Only My Issues
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton color="empty" isActive={recent} onClick={() => setRecent(!recent)}>
|
||||||
|
Recently Updated
|
||||||
|
</StyledButton>
|
||||||
|
{!areFiltersCleared && <ClearAll onClick={clearFilters}>Clear all</ClearAll>}
|
||||||
|
</Filters>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectBoardFilters.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoardFilters;
|
||||||
26
client/src/components/Project/Board/Header/Styles.js
Normal file
26
client/src/components/Project/Board/Header/Styles.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { color, font } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const Breadcrumbs = styled.div`
|
||||||
|
color: ${color.textMedium};
|
||||||
|
${font.size(15)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Divider = styled.span`
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
margin: 0 10px;
|
||||||
|
${font.size(18)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Header = styled.div`
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const BoardName = styled.div`
|
||||||
|
${font.size(24)}
|
||||||
|
${font.medium}
|
||||||
|
`;
|
||||||
42
client/src/components/Project/Board/Header/index.jsx
Normal file
42
client/src/components/Project/Board/Header/index.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { copyToClipboard } from 'shared/utils/clipboard';
|
||||||
|
import { Button } from 'shared/components';
|
||||||
|
import { Breadcrumbs, Divider, Header, BoardName } from './Styles';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
projectName: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectBoardHeader = ({ projectName }) => {
|
||||||
|
const [isLinkCopied, setLinkCopied] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Breadcrumbs>
|
||||||
|
Projects
|
||||||
|
<Divider>/</Divider>
|
||||||
|
{projectName}
|
||||||
|
<Divider>/</Divider>
|
||||||
|
Kanban Board
|
||||||
|
</Breadcrumbs>
|
||||||
|
<Header>
|
||||||
|
<BoardName>Kanban board</BoardName>
|
||||||
|
<Button
|
||||||
|
icon="link"
|
||||||
|
onClick={() => {
|
||||||
|
setLinkCopied(true);
|
||||||
|
setTimeout(() => setLinkCopied(false), 2000);
|
||||||
|
copyToClipboard(window.location.href);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLinkCopied ? 'Link Copied' : 'Copy link'}
|
||||||
|
</Button>
|
||||||
|
</Header>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectBoardHeader.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoardHeader;
|
||||||
80
client/src/components/Project/Board/Lists/Styles.js
Normal file
80
client/src/components/Project/Board/Lists/Styles.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { Avatar, Icon } from 'shared/components';
|
||||||
|
import { color, issueTypeColors, issuePriorityColors, font, mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const Lists = styled.div`
|
||||||
|
display: flex;
|
||||||
|
margin: 26px -5px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const List = styled.div`
|
||||||
|
margin: 0 5px;
|
||||||
|
width: 25%;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: ${color.backgroundLightest};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ListTitle = styled.div`
|
||||||
|
padding: 13px 10px 17px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: ${color.textMedium};
|
||||||
|
${font.size(12.5)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ListIssuesCount = styled.span`
|
||||||
|
text-transform: lowercase;
|
||||||
|
${font.size(13)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Issues = styled.div`
|
||||||
|
padding: 0 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Issue = styled.div`
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0px 1px 2px 0px rgba(9, 30, 66, 0.25);
|
||||||
|
transition: background 0.1s;
|
||||||
|
${mixin.clickable}
|
||||||
|
&:hover {
|
||||||
|
background: ${color.backgroundLight};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const IssueTitle = styled.p`
|
||||||
|
padding-bottom: 11px;
|
||||||
|
${font.size(15)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const IssueBottom = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const IssueTypeIcon = styled(Icon)`
|
||||||
|
font-size: 19px;
|
||||||
|
color: ${props => issueTypeColors[props.color]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const IssuePriorityIcon = styled(Icon)`
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: ${props => issuePriorityColors[props.color]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const IssueAssignees = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
margin-left: 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const IssueAssigneeAvatar = styled(Avatar)`
|
||||||
|
margin-left: -2px;
|
||||||
|
box-shadow: 0 0 0 2px #fff;
|
||||||
|
`;
|
||||||
92
client/src/components/Project/Board/Lists/index.jsx
Normal file
92
client/src/components/Project/Board/Lists/index.jsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { IssueStatus, IssuePriority } from 'shared/constants/issues';
|
||||||
|
import {
|
||||||
|
Lists,
|
||||||
|
List,
|
||||||
|
ListTitle,
|
||||||
|
ListIssuesCount,
|
||||||
|
Issues,
|
||||||
|
Issue,
|
||||||
|
IssueTitle,
|
||||||
|
IssueBottom,
|
||||||
|
IssueTypeIcon,
|
||||||
|
IssuePriorityIcon,
|
||||||
|
IssueAssignees,
|
||||||
|
IssueAssigneeAvatar,
|
||||||
|
} from './Styles';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
project: PropTypes.object.isRequired,
|
||||||
|
filteredIssues: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectBoardLists = ({ project, filteredIssues }) => {
|
||||||
|
const renderList = status => {
|
||||||
|
const getListIssues = issues => issues.filter(issue => issue.status === status);
|
||||||
|
const allListIssues = getListIssues(project.issues);
|
||||||
|
const filteredListIssues = getListIssues(filteredIssues);
|
||||||
|
|
||||||
|
const issuesCount =
|
||||||
|
allListIssues.length !== filteredListIssues.length
|
||||||
|
? `${filteredListIssues.length} of ${allListIssues.length}`
|
||||||
|
: allListIssues.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List key={status}>
|
||||||
|
<ListTitle>
|
||||||
|
{`${issueStatusCopy[status]} `}
|
||||||
|
<ListIssuesCount>{issuesCount}</ListIssuesCount>
|
||||||
|
</ListTitle>
|
||||||
|
<Issues>{filteredListIssues.map(renderIssue)}</Issues>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderIssue = issue => {
|
||||||
|
const getUserById = userId => project.users.find(user => user.id === userId);
|
||||||
|
const assignees = issue.userIds.map(getUserById);
|
||||||
|
return (
|
||||||
|
<Issue key={issue.id}>
|
||||||
|
<IssueTitle>{issue.title}</IssueTitle>
|
||||||
|
<IssueBottom>
|
||||||
|
<div>
|
||||||
|
<IssueTypeIcon type={issue.type} color={issue.type} />
|
||||||
|
<IssuePriorityIcon
|
||||||
|
type={
|
||||||
|
[IssuePriority.LOW || IssuePriority.LOWEST].includes(issue.priority)
|
||||||
|
? 'arrow-down'
|
||||||
|
: 'arrow-up'
|
||||||
|
}
|
||||||
|
color={issue.priority}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<IssueAssignees>
|
||||||
|
{assignees.map(user => (
|
||||||
|
<IssueAssigneeAvatar
|
||||||
|
key={user.id}
|
||||||
|
size={24}
|
||||||
|
avatarUrl={user.avatarUrl}
|
||||||
|
name={user.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</IssueAssignees>
|
||||||
|
</IssueBottom>
|
||||||
|
</Issue>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Lists>{Object.values(IssueStatus).map(renderList)}</Lists>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const issueStatusCopy = {
|
||||||
|
[IssueStatus.BACKLOG]: 'Backlog',
|
||||||
|
[IssueStatus.SELECTED]: 'Selected for development',
|
||||||
|
[IssueStatus.INPROGRESS]: 'In progress',
|
||||||
|
[IssueStatus.DONE]: 'Done',
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectBoardLists.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoardLists;
|
||||||
32
client/src/components/Project/Board/index.jsx
Normal file
32
client/src/components/Project/Board/index.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import useApi from 'shared/hooks/api';
|
||||||
|
import Header from './Header';
|
||||||
|
import Filters from './Filters';
|
||||||
|
import Lists from './Lists';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
project: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectBoard = ({ project }) => {
|
||||||
|
const [filteredIssues, setFilteredIssues] = useState([]);
|
||||||
|
|
||||||
|
const [{ data }] = useApi.get('/currentUser');
|
||||||
|
|
||||||
|
const { currentUser } = data || {};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header projectName={project.name} />
|
||||||
|
{currentUser && (
|
||||||
|
<Filters project={project} currentUser={currentUser} onChange={setFilteredIssues} />
|
||||||
|
)}
|
||||||
|
<Lists project={project} filteredIssues={filteredIssues} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectBoard.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoard;
|
||||||
56
client/src/components/Project/Sidebar/Styles.js
Normal file
56
client/src/components/Project/Sidebar/Styles.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { color, sizes, font, mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const Sidebar = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: ${sizes.appNavBarLeftWidth}px;
|
||||||
|
height: 100vh;
|
||||||
|
width: 240px;
|
||||||
|
padding: 0 16px;
|
||||||
|
background: ${color.backgroundLightest};
|
||||||
|
border-right: 1px solid ${color.borderLightest};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ProjectInfo = styled.div`
|
||||||
|
display: flex;
|
||||||
|
padding: 24px 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ProjectTexts = styled.div`
|
||||||
|
padding: 3px 0 0 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ProjectName = styled.div`
|
||||||
|
color: ${color.textDark};
|
||||||
|
${font.size(15)};
|
||||||
|
${font.medium};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ProjectCategory = styled.div`
|
||||||
|
color: ${color.textMedium};
|
||||||
|
${font.size(13)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LinkItem = styled(Link)`
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: ${color.textDark};
|
||||||
|
${mixin.clickable}
|
||||||
|
&:hover {
|
||||||
|
background: ${color.backgroundLight};
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
margin-right: 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: ${color.textDarkest};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LinkText = styled.div`
|
||||||
|
padding-top: 2px;
|
||||||
|
${font.size(15)};
|
||||||
|
`;
|
||||||
45
client/src/components/Project/Sidebar/index.jsx
Normal file
45
client/src/components/Project/Sidebar/index.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Icon, ProjectAvatar } from 'shared/components';
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
ProjectInfo,
|
||||||
|
ProjectTexts,
|
||||||
|
ProjectName,
|
||||||
|
ProjectCategory,
|
||||||
|
LinkItem,
|
||||||
|
LinkText,
|
||||||
|
} from './Styles';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
projectName: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectSidebar = ({ projectName }) => (
|
||||||
|
<Sidebar>
|
||||||
|
<ProjectInfo>
|
||||||
|
<ProjectAvatar />
|
||||||
|
<ProjectTexts>
|
||||||
|
<ProjectName>{projectName}</ProjectName>
|
||||||
|
<ProjectCategory>Software project</ProjectCategory>
|
||||||
|
</ProjectTexts>
|
||||||
|
</ProjectInfo>
|
||||||
|
<LinkItem to="/project/board">
|
||||||
|
<Icon type="board" />
|
||||||
|
<LinkText>Kanban Board</LinkText>
|
||||||
|
</LinkItem>
|
||||||
|
<LinkItem to="/project/issues">
|
||||||
|
<Icon type="issues" />
|
||||||
|
<LinkText>Issues and filters</LinkText>
|
||||||
|
</LinkItem>
|
||||||
|
<LinkItem to="/project/settings">
|
||||||
|
<Icon type="settings" />
|
||||||
|
<LinkText>Project settings</LinkText>
|
||||||
|
</LinkItem>
|
||||||
|
</Sidebar>
|
||||||
|
);
|
||||||
|
|
||||||
|
ProjectSidebar.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectSidebar;
|
||||||
7
client/src/components/Project/Styles.js
Normal file
7
client/src/components/Project/Styles.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { sizes } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const ProjectPage = styled.div`
|
||||||
|
padding: 25px 32px 0 ${sizes.secondarySideBarWidth + 40}px;
|
||||||
|
`;
|
||||||
24
client/src/components/Project/index.jsx
Normal file
24
client/src/components/Project/index.jsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import useApi from 'shared/hooks/api';
|
||||||
|
import { PageLoader, PageError } from 'shared/components';
|
||||||
|
import Sidebar from './Sidebar';
|
||||||
|
import Board from './Board';
|
||||||
|
import { ProjectPage } from './Styles';
|
||||||
|
|
||||||
|
const Project = () => {
|
||||||
|
const [{ data, error, isLoading }] = useApi.get('/project');
|
||||||
|
|
||||||
|
if (isLoading) return <PageLoader />;
|
||||||
|
if (error) return <PageError />;
|
||||||
|
|
||||||
|
const { project } = data;
|
||||||
|
return (
|
||||||
|
<ProjectPage>
|
||||||
|
<Sidebar projectName={project.name} />
|
||||||
|
<Board project={project} />
|
||||||
|
</ProjectPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Project;
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 97 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 21 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,8 +7,7 @@ export const Image = styled.div`
|
|||||||
width: ${props => props.size}px;
|
width: ${props => props.size}px;
|
||||||
height: ${props => props.size}px;
|
height: ${props => props.size}px;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
background-image: url('${props => props.avatarUrl}');
|
${props => mixin.backgroundImage(props.avatarUrl)}
|
||||||
${mixin.backgroundImage}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Letter = styled.div`
|
export const Letter = styled.div`
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const defaultProps = {
|
|||||||
className: undefined,
|
className: undefined,
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
name: '',
|
name: '',
|
||||||
size: 24,
|
size: 32,
|
||||||
};
|
};
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
@@ -30,12 +30,12 @@ const colors = [
|
|||||||
|
|
||||||
const getColorFromName = name => colors[name.toLocaleLowerCase().charCodeAt(0) % colors.length];
|
const getColorFromName = name => colors[name.toLocaleLowerCase().charCodeAt(0) % colors.length];
|
||||||
|
|
||||||
const Avatar = ({ className, avatarUrl, name, size }) => {
|
const Avatar = ({ className, avatarUrl, name, size, ...otherProps }) => {
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
return <Image className={className} size={size} avatarUrl={avatarUrl} />;
|
return <Image className={className} size={size} avatarUrl={avatarUrl} {...otherProps} />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Letter className={className} size={size} color={getColorFromName(name)}>
|
<Letter className={className} size={size} color={getColorFromName(name)} {...otherProps}>
|
||||||
<span>{name.charAt(0)}</span>
|
<span>{name.charAt(0)}</span>
|
||||||
</Letter>
|
</Letter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,83 +4,79 @@ import Spinner from 'shared/components/Spinner';
|
|||||||
import { color, font, mixin } from 'shared/utils/styles';
|
import { color, font, mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
export const StyledButton = styled.button`
|
export const StyledButton = styled.button`
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
height: 36px;
|
align-items: center;
|
||||||
line-height: 34px;
|
justify-content: center;
|
||||||
padding: 0 18px;
|
height: 32px;
|
||||||
vertical-align: middle;
|
line-height: 1;
|
||||||
|
padding: 0 ${props => (props.iconOnly ? 9 : 12)}px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: center;
|
border-radius: 3px;
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.1s;
|
transition: all 0.1s;
|
||||||
appearance: none !important;
|
appearance: none;
|
||||||
${mixin.clickable}
|
${mixin.clickable}
|
||||||
${font.bold}
|
${font.size(14.5)}
|
||||||
${font.size(14)}
|
${props => buttonColors[props.color]}
|
||||||
${props => (props.hollow ? hollowStyles : filledStyles)}
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
i {
|
i {
|
||||||
position: relative;
|
margin-right: ${props => (props.iconOnly ? 0 : 7)}px;
|
||||||
top: -1px;
|
|
||||||
right: 4px;
|
|
||||||
margin-right: 7px;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
${props => (props.iconOnly ? iconOnlyStyles : '')}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const filledStyles = props => css`
|
|
||||||
color: #fff;
|
|
||||||
background: ${color[props.color]};
|
|
||||||
border: 1px solid ${color[props.color]};
|
|
||||||
${!props.disabled &&
|
|
||||||
css`
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
background: ${mixin.darken(color[props.color], 0.15)};
|
|
||||||
border: 1px solid ${mixin.darken(color[props.color], 0.15)};
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background: ${mixin.lighten(color[props.color], 0.1)};
|
|
||||||
border: 1px solid ${mixin.lighten(color[props.color], 0.1)};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const hollowStyles = props => css`
|
|
||||||
color: ${color.textMediumBlue};
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid ${color.borderBlue};
|
|
||||||
${!props.disabled &&
|
|
||||||
css`
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
border: 1px solid ${mixin.darken(color.borderBlue, 0.15)};
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
border: 1px solid ${color.borderBlue};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const iconOnlyStyles = css`
|
|
||||||
padding: 0 12px;
|
|
||||||
i {
|
|
||||||
right: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const secondaryAndEmptyShared = css`
|
||||||
|
color: ${color.textDark};
|
||||||
|
${font.regular}
|
||||||
|
&:not(:disabled) {
|
||||||
|
&:hover {
|
||||||
|
background: ${color.backgroundLight};
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: ${color.primary};
|
||||||
|
background: ${mixin.rgba(color.primary, 0.15)};
|
||||||
|
}
|
||||||
|
${props =>
|
||||||
|
props.isActive &&
|
||||||
|
`
|
||||||
|
color: ${color.primary};
|
||||||
|
background: ${mixin.rgba(color.primary, 0.15)} !important;
|
||||||
|
`}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const buttonColors = {
|
||||||
|
primary: css`
|
||||||
|
color: #fff;
|
||||||
|
background: ${color.primary};
|
||||||
|
${font.medium}
|
||||||
|
&:not(:disabled) {
|
||||||
|
&:hover {
|
||||||
|
background: ${mixin.lighten(color.primary, 0.15)};
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background: ${mixin.darken(color.primary, 0.1)};
|
||||||
|
}
|
||||||
|
${props =>
|
||||||
|
props.isActive &&
|
||||||
|
`
|
||||||
|
background: ${mixin.darken(color.primary, 0.1)} !important;
|
||||||
|
`}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
secondary: css`
|
||||||
|
background: ${color.secondary};
|
||||||
|
${secondaryAndEmptyShared};
|
||||||
|
`,
|
||||||
|
empty: css`
|
||||||
|
background: #fff;
|
||||||
|
${secondaryAndEmptyShared};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
export const StyledSpinner = styled(Spinner)`
|
export const StyledSpinner = styled(Spinner)`
|
||||||
position: relative;
|
position: relative;
|
||||||
right: 8px;
|
top: 1px;
|
||||||
display: inline-block;
|
margin-right: ${props => (props.iconOnly ? 0 : 7)}px;
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 1;
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import { StyledButton, StyledSpinner } from './Styles';
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
type: PropTypes.string,
|
color: PropTypes.oneOf(['primary', 'secondary', 'empty']),
|
||||||
hollow: PropTypes.bool,
|
|
||||||
color: PropTypes.oneOf(['primary', 'success', 'danger']),
|
|
||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
iconSize: PropTypes.number,
|
iconSize: PropTypes.number,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
@@ -21,11 +19,9 @@ const propTypes = {
|
|||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
className: undefined,
|
className: undefined,
|
||||||
children: undefined,
|
children: undefined,
|
||||||
type: 'button',
|
color: 'secondary',
|
||||||
hollow: false,
|
|
||||||
color: 'primary',
|
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
iconSize: undefined,
|
iconSize: 18,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
working: false,
|
working: false,
|
||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
@@ -33,7 +29,7 @@ const defaultProps = {
|
|||||||
|
|
||||||
const Button = ({
|
const Button = ({
|
||||||
children,
|
children,
|
||||||
hollow,
|
color: propsColor,
|
||||||
icon,
|
icon,
|
||||||
iconSize,
|
iconSize,
|
||||||
disabled,
|
disabled,
|
||||||
@@ -43,21 +39,31 @@ const Button = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<StyledButton
|
<StyledButton
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
hollow={hollow}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!disabled && !working) {
|
if (!disabled && !working) {
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
color={propsColor}
|
||||||
disabled={disabled || working}
|
disabled={disabled || working}
|
||||||
working={working}
|
working={working}
|
||||||
iconOnly={!children}
|
iconOnly={!children}
|
||||||
>
|
>
|
||||||
{working && <StyledSpinner size={26} color={hollow ? color.textMediumBlue : '#fff'} />}
|
{working && (
|
||||||
{!working && icon && (
|
<StyledSpinner
|
||||||
<Icon type={icon} size={iconSize} color={hollow ? color.textMediumBlue : '#fff'} />
|
iconOnly={!children}
|
||||||
|
size={26}
|
||||||
|
color={propsColor === 'primary' ? '#fff' : color.textDark}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{children}
|
{!working && icon && (
|
||||||
|
<Icon
|
||||||
|
type={icon}
|
||||||
|
size={iconSize}
|
||||||
|
color={propsColor === 'primary' ? '#fff' : color.textDark}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>{children}</div>
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const ConfirmModal = ({
|
|||||||
{confirmInput && (
|
{confirmInput && (
|
||||||
<>
|
<>
|
||||||
<InputLabel>{`Type ${confirmInput} below to confirm.`}</InputLabel>
|
<InputLabel>{`Type ${confirmInput} below to confirm.`}</InputLabel>
|
||||||
<StyledInput onChange={(event, value) => handleConfirmInputChange(value)} />
|
<StyledInput onChange={handleConfirmInputChange} />
|
||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const Dropdown = styled.div`
|
|||||||
top: 130%;
|
top: 130%;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 270px;
|
width: 270px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
${mixin.boxShadowBorderMedium}
|
${mixin.boxShadowBorderMedium}
|
||||||
${props => (props.withTime ? withTimeStyles : '')}
|
${props => (props.withTime ? withTimeStyles : '')}
|
||||||
@@ -76,7 +76,7 @@ export const Day = styled.div`
|
|||||||
width: 14.28%;
|
width: 14.28%;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
${font.size(15)}
|
${font.size(15)}
|
||||||
${props => (!props.isFiller ? hoverStyles : '')}
|
${props => (!props.isFiller ? hoverStyles : '')}
|
||||||
${props => (props.isToday ? font.bold : '')}
|
${props => (props.isToday ? font.bold : '')}
|
||||||
|
|||||||
@@ -4,42 +4,30 @@ import PropTypes from 'prop-types';
|
|||||||
import StyledIcon from './Styles';
|
import StyledIcon from './Styles';
|
||||||
|
|
||||||
const codes = {
|
const codes = {
|
||||||
[`check-circle`]: '\\e86c',
|
[`bug`]: '\\e90f',
|
||||||
[`check-fat`]: '\\f00c',
|
[`stopwatch`]: '\\e914',
|
||||||
[`arrow-left`]: '\\e900',
|
[`task`]: '\\e910',
|
||||||
[`arrow-right`]: '\\e912',
|
[`story`]: '\\e911',
|
||||||
[`upload-thin`]: '\\e91f',
|
[`arrow-down`]: '\\e90a',
|
||||||
[`bell`]: '\\e901',
|
[`arrow-left-circle`]: '\\e917',
|
||||||
[`calendar`]: '\\e903',
|
[`arrow-up`]: '\\e90b',
|
||||||
[`check`]: '\\e904',
|
[`chevron-down`]: '\\e900',
|
||||||
[`chevron-down`]: '\\e905',
|
[`chevron-left`]: '\\e901',
|
||||||
[`chevron-left`]: '\\e906',
|
[`chevron-right`]: '\\e902',
|
||||||
[`chevron-right`]: '\\e907',
|
[`chevron-up`]: '\\e903',
|
||||||
[`chevron-up`]: '\\e908',
|
[`board`]: '\\e904',
|
||||||
[`clock`]: '\\e909',
|
[`help`]: '\\e905',
|
||||||
[`download`]: '\\e90a',
|
[`link`]: '\\e90c',
|
||||||
[`plus`]: '\\e90c',
|
[`menu`]: '\\e916',
|
||||||
[`refresh`]: '\\e90d',
|
[`more`]: '\\e90e',
|
||||||
[`search`]: '\\e90e',
|
[`attach`]: '\\e90d',
|
||||||
[`upload`]: '\\e90f',
|
[`plus`]: '\\e906',
|
||||||
[`close`]: '\\e910',
|
[`search`]: '\\e907',
|
||||||
[`archive`]: '\\e915',
|
[`issues`]: '\\e908',
|
||||||
[`briefcase`]: '\\e916',
|
[`settings`]: '\\e909',
|
||||||
[`settings`]: '\\e902',
|
[`close`]: '\\e913',
|
||||||
[`email`]: '\\e914',
|
[`help-filled`]: '\\e912',
|
||||||
[`lock`]: '\\e913',
|
[`feedback`]: '\\e915',
|
||||||
[`dashboard`]: '\\e917',
|
|
||||||
[`alert`]: '\\e911',
|
|
||||||
[`edit`]: '\\e918',
|
|
||||||
[`delete`]: '\\e919',
|
|
||||||
[`sort`]: '\\f0dc',
|
|
||||||
[`sort-up`]: '\\f0d8',
|
|
||||||
[`sort-down`]: '\\f0d7',
|
|
||||||
[`euro`]: '\\f153',
|
|
||||||
[`folder-plus`]: '\\e921',
|
|
||||||
[`folder-minus`]: '\\e920',
|
|
||||||
[`file`]: '\\e90b',
|
|
||||||
[`file-text`]: '\\e924',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
|||||||
@@ -5,29 +5,29 @@ import { color, font } from 'shared/utils/styles';
|
|||||||
export default styled.div`
|
export default styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 40px;
|
height: 32px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
input {
|
input {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 15px;
|
padding: 0 7px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
border: 1px solid ${color.borderLight};
|
border: 1px solid ${color.borderLightest};
|
||||||
box-shadow: inset 0 0 1px 0 rgba(0, 0, 0, 0.03);
|
background: ${color.backgroundLightest};
|
||||||
background: #fff;
|
|
||||||
${font.regular}
|
${font.regular}
|
||||||
${font.size(14)}
|
${font.size(15)}
|
||||||
&:focus {
|
&:focus {
|
||||||
border: 1px solid ${color.borderMedium};
|
background: #fff;
|
||||||
|
border: 1px solid ${color.borderInputFocus};
|
||||||
|
box-shadow: 0 0 0 1px ${color.borderInputFocus};
|
||||||
}
|
}
|
||||||
${props => (props.icon ? 'padding-left: 40px;' : '')}
|
${props => (props.icon ? 'padding-left: 32px;' : '')}
|
||||||
${props => (props.invalid ? `&, &:focus { border: 1px solid ${color.danger}; }` : '')}
|
${props => (props.invalid ? `&, &:focus { border: 1px solid ${color.danger}; }` : '')}
|
||||||
}
|
}
|
||||||
i {
|
i {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 8px;
|
||||||
left: 14px;
|
left: 8px;
|
||||||
font-size: 16px;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: ${color.textMedium};
|
color: ${color.textMedium};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ const defaultProps = {
|
|||||||
const Input = forwardRef(({ icon, className, invalid, filter, onChange, ...inputProps }, ref) => {
|
const Input = forwardRef(({ icon, className, invalid, filter, onChange, ...inputProps }, ref) => {
|
||||||
const handleChange = event => {
|
const handleChange = event => {
|
||||||
if (!filter || filter.test(event.target.value)) {
|
if (!filter || filter.test(event.target.value)) {
|
||||||
onChange(event, event.target.value);
|
onChange(event.target.value, event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<StyledInput className={className} icon={icon} invalid={invalid}>
|
<StyledInput className={className} icon={icon} invalid={invalid}>
|
||||||
{icon && <Icon type={icon} />}
|
{icon && <Icon type={icon} size={15} />}
|
||||||
<input {...inputProps} onChange={handleChange} ref={ref} />
|
<input {...inputProps} onChange={handleChange} ref={ref} />
|
||||||
</StyledInput>
|
</StyledInput>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
width: PropTypes.number,
|
size: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
className: undefined,
|
className: undefined,
|
||||||
width: 28,
|
size: 28,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Logo = ({ className, width }) => (
|
const Logo = ({ className, size }) => (
|
||||||
<span className={className}>
|
<span className={className}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.76 75.76" width={width}>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.76 75.76" width={size}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="linear-gradient"
|
id="linear-gradient"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const ScrollOverlay = styled.div`
|
|||||||
|
|
||||||
export const ClickableOverlay = styled.div`
|
export const ClickableOverlay = styled.div`
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background: ${mixin.rgba(color.textLightBlue, 0.7)};
|
background: ${mixin.rgba(color.textLight, 0.7)};
|
||||||
${props => clickOverlayStyles[props.variant]}
|
${props => clickOverlayStyles[props.variant]}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
44
client/src/shared/components/PageError/Styles.js
Normal file
44
client/src/shared/components/PageError/Styles.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { Icon } from 'shared/components';
|
||||||
|
import { color, font, mixin } from 'shared/utils/styles';
|
||||||
|
import imageBackground from './assets/background-forest.jpg';
|
||||||
|
|
||||||
|
export const ErrorPage = styled.div`
|
||||||
|
padding: 64px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ErrorPageInner = styled.div`
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 1440px;
|
||||||
|
padding: 200px 0;
|
||||||
|
${mixin.backgroundImage(imageBackground)}
|
||||||
|
@media (max-height: 680px) {
|
||||||
|
padding: 140px 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ErrorBox = styled.div`
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 480px;
|
||||||
|
padding: 32px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid ${color.borderLight};
|
||||||
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledIcon = styled(Icon)`
|
||||||
|
position: absolute;
|
||||||
|
top: 32px;
|
||||||
|
left: 32px;
|
||||||
|
font-size: 30px;
|
||||||
|
color: ${color.primary};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Title = styled.h1`
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-left: 42px;
|
||||||
|
${font.size(29)}
|
||||||
|
`;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 551 KiB |
21
client/src/shared/components/PageError/index.jsx
Normal file
21
client/src/shared/components/PageError/index.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ErrorPage, ErrorPageInner, ErrorBox, StyledIcon, Title } from './Styles';
|
||||||
|
|
||||||
|
const PageError = () => (
|
||||||
|
<ErrorPage>
|
||||||
|
<ErrorPageInner>
|
||||||
|
<ErrorBox>
|
||||||
|
<StyledIcon type="bug" />
|
||||||
|
<Title>There’s been a glitch…</Title>
|
||||||
|
<p>
|
||||||
|
{'We’re not quite sure what went wrong. Please contact us or try looking on our '}
|
||||||
|
<a href="https://support.atlassian.com/jira-software-cloud/">Help Center</a>
|
||||||
|
{' if you need a hand.'}
|
||||||
|
</p>
|
||||||
|
</ErrorBox>
|
||||||
|
</ErrorPageInner>
|
||||||
|
</ErrorPage>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PageError;
|
||||||
@@ -2,6 +2,6 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
export default styled.div`
|
export default styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 100px;
|
padding-top: 200px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`;
|
||||||
|
|||||||
120
client/src/shared/components/ProjectAvatar.jsx
Normal file
120
client/src/shared/components/ProjectAvatar.jsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
size: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
className: undefined,
|
||||||
|
size: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Logo = ({ className, size }) => (
|
||||||
|
<span className={className}>
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ borderRadius: 3 }}
|
||||||
|
viewBox="0 0 128 128"
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<rect id="path-1" x="0" y="0" width="128" height="128" />
|
||||||
|
</defs>
|
||||||
|
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||||
|
<g id="project_avatar_settings">
|
||||||
|
<g>
|
||||||
|
<mask id="mask-2" fill="white">
|
||||||
|
<use xlinkHref="#path-1" />
|
||||||
|
</mask>
|
||||||
|
<use id="Rectangle" fill="#FF5630" xlinkHref="#path-1" />
|
||||||
|
<g id="Settings" fillRule="nonzero">
|
||||||
|
<g transform="translate(20.000000, 17.000000)">
|
||||||
|
<path
|
||||||
|
d="M74.578,84.289 L72.42,84.289 C70.625,84.289 69.157,82.821 69.157,81.026 L69.157,16.537 C69.157,14.742 70.625,13.274 72.42,13.274 L74.578,13.274 C76.373,13.274 77.841,14.742 77.841,16.537 L77.841,81.026 C77.842,82.82 76.373,84.289 74.578,84.289 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#2A5083"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14.252,84.289 L12.094,84.289 C10.299,84.289 8.831,82.821 8.831,81.026 L8.831,16.537 C8.831,14.742 10.299,13.274 12.094,13.274 L14.252,13.274 C16.047,13.274 17.515,14.742 17.515,16.537 L17.515,81.026 C17.515,82.82 16.047,84.289 14.252,84.289 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#2A5083"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
id="Rectangle-path"
|
||||||
|
fill="#153A56"
|
||||||
|
x="8.83"
|
||||||
|
y="51.311"
|
||||||
|
width="8.685"
|
||||||
|
height="7.763"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M13.173,53.776 L13.173,53.776 C6.342,53.776 0.753,48.187 0.753,41.356 L0.753,41.356 C0.753,34.525 6.342,28.936 13.173,28.936 L13.173,28.936 C20.004,28.936 25.593,34.525 25.593,41.356 L25.593,41.356 C25.593,48.187 20.004,53.776 13.173,53.776 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M18.021,43.881 L8.324,43.881 C7.453,43.881 6.741,43.169 6.741,42.298 L6.741,41.25 C6.741,40.379 7.453,39.667 8.324,39.667 L18.021,39.667 C18.892,39.667 19.604,40.379 19.604,41.25 L19.604,42.297 C19.605,43.168 18.892,43.881 18.021,43.881 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#2A5083"
|
||||||
|
opacity="0.2"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
id="Rectangle-path"
|
||||||
|
fill="#153A56"
|
||||||
|
x="69.157"
|
||||||
|
y="68.307"
|
||||||
|
width="8.685"
|
||||||
|
height="7.763"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M73.499,70.773 L73.499,70.773 C66.668,70.773 61.079,65.184 61.079,58.353 L61.079,58.353 C61.079,51.522 66.668,45.933 73.499,45.933 L73.499,45.933 C80.33,45.933 85.919,51.522 85.919,58.353 L85.919,58.353 C85.919,65.183 80.33,70.773 73.499,70.773 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M78.348,60.877 L68.651,60.877 C67.78,60.877 67.068,60.165 67.068,59.294 L67.068,58.247 C67.068,57.376 67.781,56.664 68.651,56.664 L78.348,56.664 C79.219,56.664 79.931,57.377 79.931,58.247 L79.931,59.294 C79.931,60.165 79.219,60.877 78.348,60.877 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#2A5083"
|
||||||
|
opacity="0.2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M44.415,84.289 L42.257,84.289 C40.462,84.289 38.994,82.821 38.994,81.026 L38.994,16.537 C38.994,14.742 40.462,13.274 42.257,13.274 L44.415,13.274 C46.21,13.274 47.678,14.742 47.678,16.537 L47.678,81.026 C47.678,82.82 46.21,84.289 44.415,84.289 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#2A5083"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
id="Rectangle-path"
|
||||||
|
fill="#153A56"
|
||||||
|
x="38.974"
|
||||||
|
y="23.055"
|
||||||
|
width="8.685"
|
||||||
|
height="7.763"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M43.316,25.521 L43.316,25.521 C36.485,25.521 30.896,19.932 30.896,13.101 L30.896,13.101 C30.896,6.27 36.485,0.681 43.316,0.681 L43.316,0.681 C50.147,0.681 55.736,6.27 55.736,13.101 L55.736,13.101 C55.736,19.932 50.147,25.521 43.316,25.521 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M48.165,15.626 L38.468,15.626 C37.597,15.626 36.885,14.914 36.885,14.043 L36.885,12.996 C36.885,12.125 37.597,11.413 38.468,11.413 L48.165,11.413 C49.036,11.413 49.748,12.125 49.748,12.996 L49.748,14.043 C49.748,14.913 49.036,15.626 48.165,15.626 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#2A5083"
|
||||||
|
opacity="0.2"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
Logo.propTypes = propTypes;
|
||||||
|
Logo.defaultProps = defaultProps;
|
||||||
|
|
||||||
|
export default Logo;
|
||||||
@@ -64,11 +64,11 @@ const SelectDropdown = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleInputKeyDown = event => {
|
const handleInputKeyDown = event => {
|
||||||
if (event.keyCode === KeyCodes.escape) {
|
if (event.keyCode === KeyCodes.ESCAPE) {
|
||||||
handleInputEscapeKeyDown(event);
|
handleInputEscapeKeyDown(event);
|
||||||
} else if (event.keyCode === KeyCodes.enter) {
|
} else if (event.keyCode === KeyCodes.ENTER) {
|
||||||
handleInputEnterKeyDown(event);
|
handleInputEnterKeyDown(event);
|
||||||
} else if (event.keyCode === KeyCodes.arrowDown || event.keyCode === KeyCodes.arrowUp) {
|
} else if (event.keyCode === KeyCodes.ARROW_DOWN || event.keyCode === KeyCodes.ARROW_UP) {
|
||||||
handleInputArrowUpOrDownKeyDown(event);
|
handleInputArrowUpOrDownKeyDown(event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -101,7 +101,7 @@ const SelectDropdown = ({
|
|||||||
const $optionsHeight = $options.getBoundingClientRect().height;
|
const $optionsHeight = $options.getBoundingClientRect().height;
|
||||||
const $activeHeight = $active.getBoundingClientRect().height;
|
const $activeHeight = $active.getBoundingClientRect().height;
|
||||||
|
|
||||||
if (event.keyCode === KeyCodes.arrowDown) {
|
if (event.keyCode === KeyCodes.ARROW_DOWN) {
|
||||||
if ($options.lastElementChild === $active) {
|
if ($options.lastElementChild === $active) {
|
||||||
$active.classList.remove(activeOptionClass);
|
$active.classList.remove(activeOptionClass);
|
||||||
$options.firstElementChild.classList.add(activeOptionClass);
|
$options.firstElementChild.classList.add(activeOptionClass);
|
||||||
@@ -113,7 +113,7 @@ const SelectDropdown = ({
|
|||||||
$options.scrollTop += $activeHeight;
|
$options.scrollTop += $activeHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event.keyCode === KeyCodes.arrowUp) {
|
} else if (event.keyCode === KeyCodes.ARROW_UP) {
|
||||||
if ($options.firstElementChild === $active) {
|
if ($options.firstElementChild === $active) {
|
||||||
$active.classList.remove(activeOptionClass);
|
$active.classList.remove(activeOptionClass);
|
||||||
$options.lastElementChild.classList.add(activeOptionClass);
|
$options.lastElementChild.classList.add(activeOptionClass);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Icon from 'shared/components/Icon';
|
|||||||
export const StyledSelect = styled.div`
|
export const StyledSelect = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
border: 1px solid ${color.borderLight};
|
border: 1px solid ${color.borderLight};
|
||||||
background: #fff;
|
background: #fff;
|
||||||
${font.size(14)}
|
${font.size(14)}
|
||||||
@@ -41,7 +41,7 @@ export const ChevronIcon = styled(Icon)`
|
|||||||
|
|
||||||
export const Placeholder = styled.div`
|
export const Placeholder = styled.div`
|
||||||
padding: 11px 0 0 15px;
|
padding: 11px 0 0 15px;
|
||||||
color: ${color.textLightBlue};
|
color: ${color.textLight};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ValueSingle = styled.div`
|
export const ValueSingle = styled.div`
|
||||||
|
|||||||
@@ -93,10 +93,10 @@ const Select = ({
|
|||||||
const handleFocusedSelectKeydown = event => {
|
const handleFocusedSelectKeydown = event => {
|
||||||
if (isDropdownOpen) return;
|
if (isDropdownOpen) return;
|
||||||
|
|
||||||
if (event.keyCode === KeyCodes.enter) {
|
if (event.keyCode === KeyCodes.ENTER) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
if (event.keyCode !== KeyCodes.escape && event.keyCode !== KeyCodes.tab && !event.shiftKey) {
|
if (event.keyCode !== KeyCodes.ESCAPE && event.keyCode !== KeyCodes.TAB && !event.shiftKey) {
|
||||||
setDropdownOpen(true);
|
setDropdownOpen(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ export default styled.div`
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
|
||||||
padding: 13px 15px 14px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid ${color.borderLight};
|
|
||||||
box-shadow: inset 0 0 1px 0 rgba(0, 0, 0, 0.03);
|
|
||||||
background: #fff;
|
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 7px 7px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid ${color.borderLightest};
|
||||||
|
background: ${color.backgroundLightest};
|
||||||
${font.regular}
|
${font.regular}
|
||||||
${font.size(14)}
|
${font.size(15)}
|
||||||
&:focus {
|
&:focus {
|
||||||
border: 1px solid ${color.borderMedium};
|
background: #fff;
|
||||||
|
border: 1px solid ${color.borderInputFocus};
|
||||||
|
box-shadow: 0 0 0 1px ${color.borderInputFocus};
|
||||||
}
|
}
|
||||||
${props => (props.invalid ? `&, &:focus { border: 1px solid ${color.danger}; }` : '')}
|
${props => (props.invalid ? `&, &:focus { border: 1px solid ${color.danger}; }` : '')}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const Textarea = forwardRef(({ className, invalid, onChange, ...textareaProps },
|
|||||||
<StyledTextarea className={className} invalid={invalid}>
|
<StyledTextarea className={className} invalid={invalid}>
|
||||||
<TextareaAutoSize
|
<TextareaAutoSize
|
||||||
{...textareaProps}
|
{...textareaProps}
|
||||||
onChange={event => onChange(event, event.target.value)}
|
onChange={event => onChange(event.target.value, event)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
</StyledTextarea>
|
</StyledTextarea>
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ export { default as Icon } from './Icon';
|
|||||||
export { default as Input } from './Input';
|
export { default as Input } from './Input';
|
||||||
export { default as Logo } from './Logo';
|
export { default as Logo } from './Logo';
|
||||||
export { default as Modal } from './Modal';
|
export { default as Modal } from './Modal';
|
||||||
|
export { default as PageError } from './PageError';
|
||||||
export { default as PageLoader } from './PageLoader';
|
export { default as PageLoader } from './PageLoader';
|
||||||
|
export { default as ProjectAvatar } from './ProjectAvatar';
|
||||||
export { default as Select } from './Select';
|
export { default as Select } from './Select';
|
||||||
export { default as Spinner } from './Spinner';
|
export { default as Spinner } from './Spinner';
|
||||||
export { default as Textarea } from './Textarea';
|
export { default as Textarea } from './Textarea';
|
||||||
|
|||||||
20
client/src/shared/constants/issues.js
Normal file
20
client/src/shared/constants/issues.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export const IssueType = {
|
||||||
|
TASK: 'task',
|
||||||
|
BUG: 'bug',
|
||||||
|
STORY: 'story',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueStatus = {
|
||||||
|
BACKLOG: 'backlog',
|
||||||
|
SELECTED: 'selected',
|
||||||
|
INPROGRESS: 'inprogress',
|
||||||
|
DONE: 'done',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssuePriority = {
|
||||||
|
HIGHEST: '5',
|
||||||
|
HIGH: '4',
|
||||||
|
MEDIUM: '3',
|
||||||
|
LOW: '2',
|
||||||
|
LOWEST: '1',
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export const KeyCodes = {
|
export const KeyCodes = {
|
||||||
escape: 27,
|
ESCAPE: 27,
|
||||||
tab: 9,
|
TAB: 9,
|
||||||
enter: 13,
|
ENTER: 13,
|
||||||
arrowUp: 38,
|
ARROW_UP: 38,
|
||||||
arrowDown: 40,
|
ARROW_DOWN: 40,
|
||||||
};
|
};
|
||||||
|
|||||||
71
client/src/shared/hooks/api.js
Normal file
71
client/src/shared/hooks/api.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
import api from 'shared/utils/api';
|
||||||
|
import useDeepCompareMemoize from './deepCompareMemoize';
|
||||||
|
|
||||||
|
const useApi = (method, url, paramsOrData = {}, { lazy = false } = {}) => {
|
||||||
|
const isCalledAutomatically = method === 'get' && !lazy;
|
||||||
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
data: null,
|
||||||
|
error: null,
|
||||||
|
isLoading: isCalledAutomatically,
|
||||||
|
variables: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const wasCalledRef = useRef(false);
|
||||||
|
|
||||||
|
const paramsOrDataMemoized = useDeepCompareMemoize(paramsOrData);
|
||||||
|
const stateRef = useRef();
|
||||||
|
stateRef.current = state;
|
||||||
|
|
||||||
|
const makeRequest = useCallback(
|
||||||
|
(newVariables = {}) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const updateState = newState => setState({ ...stateRef.current, ...newState });
|
||||||
|
const variables = { ...stateRef.current.variables, ...newVariables };
|
||||||
|
|
||||||
|
if (!isCalledAutomatically || wasCalledRef.current) {
|
||||||
|
updateState({ variables, isLoading: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
api[method](url, { ...paramsOrDataMemoized, ...variables }).then(
|
||||||
|
data => {
|
||||||
|
resolve(data);
|
||||||
|
updateState({ data, error: null, isLoading: false });
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
reject(error);
|
||||||
|
updateState({ error, data: null, isLoading: false });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
wasCalledRef.current = true;
|
||||||
|
}),
|
||||||
|
[method, paramsOrDataMemoized, isCalledAutomatically, url],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCalledAutomatically) {
|
||||||
|
makeRequest();
|
||||||
|
}
|
||||||
|
}, [makeRequest, isCalledAutomatically]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
wasCalled: wasCalledRef.current,
|
||||||
|
variables: { ...paramsOrDataMemoized, ...state.variables },
|
||||||
|
},
|
||||||
|
makeRequest,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
get: (...args) => useApi('get', ...args),
|
||||||
|
post: (...args) => useApi('post', ...args),
|
||||||
|
put: (...args) => useApi('put', ...args),
|
||||||
|
patch: (...args) => useApi('patch', ...args),
|
||||||
|
delete: (...args) => useApi('delete', ...args),
|
||||||
|
};
|
||||||
19
client/src/shared/hooks/debounceValue.js
Normal file
19
client/src/shared/hooks/debounceValue.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const useDebounceValue = (value, delay) => {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDebounceValue;
|
||||||
14
client/src/shared/hooks/deepCompareMemoize.js
Normal file
14
client/src/shared/hooks/deepCompareMemoize.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
|
function useDeepCompareMemoize(value) {
|
||||||
|
const valueRef = useRef();
|
||||||
|
|
||||||
|
if (!isEqual(value, valueRef.current)) {
|
||||||
|
valueRef.current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDeepCompareMemoize;
|
||||||
@@ -5,7 +5,7 @@ import { KeyCodes } from 'shared/constants/keyCodes';
|
|||||||
const useOnEscapeKeyDown = (isListening, onEscapeKeyDown) => {
|
const useOnEscapeKeyDown = (isListening, onEscapeKeyDown) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = event => {
|
const handleKeyDown = event => {
|
||||||
if (event.keyCode === KeyCodes.escape) {
|
if (event.keyCode === KeyCodes.ESCAPE) {
|
||||||
onEscapeKeyDown();
|
onEscapeKeyDown();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
54
client/src/shared/utils/api.js
Normal file
54
client/src/shared/utils/api.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import history from 'browserHistory';
|
||||||
|
import { objectToQueryString } from 'shared/utils/url';
|
||||||
|
import { getStoredAuthToken, removeStoredAuthToken } from 'shared/utils/authToken';
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
baseURL: 'http://localhost:3000',
|
||||||
|
headers: () => ({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: getStoredAuthToken() ? `Bearer ${getStoredAuthToken()}` : undefined,
|
||||||
|
}),
|
||||||
|
error: {
|
||||||
|
code: 'INTERNAL_ERROR',
|
||||||
|
message: 'Something went wrong. Please check your internet connection or contact our support.',
|
||||||
|
status: 503,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = (method, url, paramsOrData) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
axios({
|
||||||
|
url: `${defaults.baseURL}${url}`,
|
||||||
|
method,
|
||||||
|
headers: defaults.headers(),
|
||||||
|
params: method === 'get' ? paramsOrData : undefined,
|
||||||
|
data: method !== 'get' ? paramsOrData : undefined,
|
||||||
|
paramsSerializer: objectToQueryString,
|
||||||
|
}).then(
|
||||||
|
response => {
|
||||||
|
resolve(response.data);
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if (error.response) {
|
||||||
|
if (error.response.data.error.code === 'INVALID_TOKEN') {
|
||||||
|
removeStoredAuthToken();
|
||||||
|
history.push('/authenticate');
|
||||||
|
} else {
|
||||||
|
reject(error.response.data.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(defaults.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
get: (...args) => api('get', ...args),
|
||||||
|
post: (...args) => api('post', ...args),
|
||||||
|
put: (...args) => api('put', ...args),
|
||||||
|
patch: (...args) => api('patch', ...args),
|
||||||
|
delete: (...args) => api('delete', ...args),
|
||||||
|
};
|
||||||
3
client/src/shared/utils/authToken.js
Normal file
3
client/src/shared/utils/authToken.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const getStoredAuthToken = () => localStorage.getItem('authToken');
|
||||||
|
export const storeAuthToken = token => localStorage.setItem('authToken', token);
|
||||||
|
export const removeStoredAuthToken = () => localStorage.removeItem('authToken');
|
||||||
8
client/src/shared/utils/clipboard.js
Normal file
8
client/src/shared/utils/clipboard.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const copyToClipboard = value => {
|
||||||
|
const $textarea = document.createElement('textarea');
|
||||||
|
$textarea.value = value;
|
||||||
|
document.body.appendChild($textarea);
|
||||||
|
$textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild($textarea);
|
||||||
|
};
|
||||||
@@ -1,33 +1,46 @@
|
|||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
|
|
||||||
export const color = {
|
export const color = {
|
||||||
primary: '#2553B3', // blue
|
primary: '#0052cc', // Blue
|
||||||
success: '#29A638', // green
|
success: '#29A638', // green
|
||||||
danger: '#E13C3C', // red
|
danger: '#E13C3C', // red
|
||||||
warning: '#F89C1C', // orange
|
warning: '#F89C1C', // orange
|
||||||
accent: '#8A46D7', // purple
|
secondary: '#F4F5F7', // light grey
|
||||||
|
|
||||||
textDarkest: '#323232',
|
textDarkest: '#172b4d',
|
||||||
textDark: '#616161',
|
textDark: '#42526E',
|
||||||
textMedium: '#75787D',
|
textMedium: '#5E6C84',
|
||||||
textMediumBlue: '#78869F',
|
textLight: '#8993a4',
|
||||||
textLight: '#959595',
|
textLink: '#0052cc',
|
||||||
textLightBlue: '#96A1B5',
|
|
||||||
|
|
||||||
backgroundDark: '#8393AD',
|
backgroundDarkPrimary: '#0747A6',
|
||||||
backgroundMedium: '#D8DDE6',
|
backgroundMedium: '#dfe1e6',
|
||||||
backgroundLight: '#F7F9FB',
|
backgroundLight: '#ebecf0',
|
||||||
|
backgroundLightest: '#F4F5F7',
|
||||||
|
|
||||||
borderLightest: '#E1E6F0',
|
borderLightest: '#dfe1e6',
|
||||||
borderLight: '#D8DDE6',
|
borderLight: '#C1C7D0',
|
||||||
borderMedium: '#B9BDC4',
|
borderInputFocus: '#4c9aff',
|
||||||
borderBlue: '#C5D3EB',
|
};
|
||||||
|
|
||||||
|
export const issueTypeColors = {
|
||||||
|
story: '#65BA43', // green
|
||||||
|
bug: '#E44D42', // red
|
||||||
|
task: '#4FADE6', // blue
|
||||||
|
};
|
||||||
|
|
||||||
|
export const issuePriorityColors = {
|
||||||
|
'5': '#CD1317', // red
|
||||||
|
'4': '#E9494A', // orange
|
||||||
|
'3': '#E97F33', // orange
|
||||||
|
'2': '#2D8738', // green
|
||||||
|
'1': '#57A55A', // green
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sizes = {
|
export const sizes = {
|
||||||
appNavBarLeftWidth: 75,
|
appNavBarLeftWidth: 64,
|
||||||
|
secondarySideBarWidth: 240,
|
||||||
minViewportWidth: 1000,
|
minViewportWidth: 1000,
|
||||||
secondarySideBarWidth: 230,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zIndexValues = {
|
export const zIndexValues = {
|
||||||
@@ -130,13 +143,14 @@ export const mixin = {
|
|||||||
background: ${background};
|
background: ${background};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
backgroundImage: `
|
backgroundImage: imageURL => `
|
||||||
|
background-image: url("${imageURL}");
|
||||||
background-position: 50% 50%;
|
background-position: 50% 50%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-color: ${color.backgroundLight};
|
background-color: ${color.backgroundLight};
|
||||||
`,
|
`,
|
||||||
link: (colorValue = color.primary) => `
|
link: (colorValue = color.textLink) => `
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: ${colorValue};
|
color: ${colorValue};
|
||||||
${font.medium}
|
${font.medium}
|
||||||
@@ -147,22 +161,4 @@ export const mixin = {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
tag: `
|
|
||||||
display: inline-block;
|
|
||||||
height: 24px;
|
|
||||||
line-height: 22px;
|
|
||||||
padding: 0 6px 0 8px;
|
|
||||||
border: 1px solid ${color.borderLight};
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background: ${color.backgroundLight};
|
|
||||||
${font.medium}
|
|
||||||
${font.size(12)}
|
|
||||||
i {
|
|
||||||
margin-left: 4px;
|
|
||||||
vertical-align: middle;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user