diff --git a/README.md b/README.md index 8026138a48490b46808aa6e15f28add9415ef739..46b660453d4bff9ae9fa6dbd20add34ab0c3f1b9 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ You can test out our web application at https://ryoko-planning.herokuapp.com/. ### Simple deployment -If you have [yarn](https://yarnpkg.com/) installed it is possible to build and start the complete -project the same way it is deployed on our server. For this simply enter the root directory of the -repository and execute the following three command in order: +If you have [yarn](https://yarnpkg.com/) or [npm](https://www.npmjs.com/) installed it is possible +to build and start the complete project the same way it is deployed on our server. +For this simply enter the root directory of the repository and execute the following three command in order: -1. `yarn install` This will install all the dependencies for both the frontend and backend -2. `yarn build` This will bundle the source for the frontend and transpile backend -3. `yarn start` This will start the web server and host the webserver at `localhost:8000` +1. `yarn install` (or `npm install`) This will install all the dependencies for both the frontend and backend +2. `yarn build` (or `npm run build`) This will bundle the source for the frontend and transpile backend +3. `yarn start` (or `npm run start`) This will start the web server and host the webserver at `localhost:8000` Note: * The server can use a public and private key pair to sign the authentication web token. They can @@ -37,7 +37,8 @@ envirenvironment variables. In any case the keys must be suitable for ES384 sign keys are not given it will use a simple password to sign the tokens. * If your `PORT` environment variable is set that will be used as the port to host the webserver in stead of port 8000. * If your `NODE_ENV` environment variable is set to `production` (with SSL) or `staging` (without SSL) the server will try -to connect to a postgres database using the connection url inside `DATABASE_URL`. +to connect to a postgres database using the connection url inside `DATABASE_URL` or +`postgresql://postgres@localhost/ryoko` if no such variable is present in the environment. ### Details @@ -48,19 +49,19 @@ server and client parts also use the same commands for running building and test Before building or running you will have to make sure that you have installed all dependencies. This can be done by executing `yarn install` (or `npm install --legacy-peer-deps`). -### How to Run +#### How to Run To start a development server you can execute `yarn start` (or `npm run start`) inside the `server` and the `client` directories. Most parts of the client will also require the server to be running simultaneously. -### How to Build +#### How to Build To build a production build enter the respective directory (either `server` or `client`) and execute `yarn build` (or `npm run build`). The build output will be created inside a directory named `build` and can then be executed using node for the server, or served staticaly for the client. -### How to Use +#### How to Use After starting the development server inside `client`, the website is accessible at `http://localhost:3000`. Depending on your configuration the site will probably be opened diff --git a/client/src/adapters/project.ts b/client/src/adapters/project.ts index 9856ae1059d0419dbc94ea43a08969997ab50f47..1fcac4becfe1960c0d301c9916e8cf06996cd6da 100644 --- a/client/src/adapters/project.ts +++ b/client/src/adapters/project.ts @@ -12,7 +12,7 @@ export enum Status { } export const StatusColors = new Map<string, string>([ - ['open', 'lightblue'], + ['open', 'blue'], ['closed', 'purple'], ['suspended', 'red'] ]); @@ -88,7 +88,7 @@ export function getProjectCompletion(uuid: string, from: Date = new Date(0), to: completion.closed + completion.suspended + completion.overdue - )}), "Failed to get project completion" + ) || 1}), "Failed to get project completion" ); } diff --git a/client/src/adapters/task.ts b/client/src/adapters/task.ts index 44f2f24bd048a053313b4b1bb8cbe0ecbf3c9afd..23a56f88d152cd5ab650e581348b4cd8c28a1983 100644 --- a/client/src/adapters/task.ts +++ b/client/src/adapters/task.ts @@ -16,7 +16,7 @@ export enum Status { } export const StatusColors = new Map<string, string>([ - ['open', 'lightblue'], + ['open', 'blue'], ['closed', 'purple'], ['suspended', 'red'] ]); diff --git a/client/src/adapters/team.ts b/client/src/adapters/team.ts index 19c9b97fd16b4453ead69e7d44e00a7608145d69..eedfb89cc1bf3423e837e4fdde592aa568953044 100644 --- a/client/src/adapters/team.ts +++ b/client/src/adapters/team.ts @@ -66,7 +66,7 @@ export function getTeamCompletion(uuid: string, from: Date = new Date(0), to: Da completion.closed + completion.suspended + completion.overdue - )}), "Failed to get team completion" + ) || 1}), "Failed to get team completion" ); } diff --git a/client/src/adapters/util.ts b/client/src/adapters/util.ts index dcaa7e1650ef645717ef6b5ed0dd98f79baf8d5f..92baca5f9a13db0f3f1b15cf03f4aa1f3f15bee3 100644 --- a/client/src/adapters/util.ts +++ b/client/src/adapters/util.ts @@ -1,7 +1,10 @@ +import { CompletionProps } from './../components/ui/Completion/index'; +import { ChartItem } from 'components/graphs/BarChart'; import { apiRoot } from 'config'; import { getAuthHeader } from './auth'; +import { StatusColors } from './task'; export interface Activity { day: string; @@ -22,13 +25,15 @@ async function executeApiRequest<T>(path: string, method: string, body: any, onS method: method, headers: { ...getAuthHeader(), - ...(body ? ( - body instanceof FormData - ? { 'Content-Type': 'multipart/form-data' } - : { 'Content-Type': 'application/json' }) + ...(body && !(body instanceof FormData) + ? { 'Content-Type': 'application/json' } : { }), }, - body: body ? JSON.stringify(body) : undefined, + body: body + ? (body instanceof FormData + ? body + : JSON.stringify(body)) + : undefined, }); if (response.ok) { return onSuccess(await response.json()); @@ -56,3 +61,35 @@ export function executeApiPut<T>(path: string, body: any, onSuccess: (data: any) return executeApiRequest(path, 'PUT', body, onSuccess, errorMessage); } +export function parseCompletion(completion: Completion): CompletionProps[] { + const allAmount = completion.sum ?? 1; + return [ + { + label: 'Closed', + percent: completion.closed / allAmount * 100, + color: StatusColors.get('closed') ?? '' + }, + { + label: 'Open', + percent: completion.open / allAmount * 100, + color: StatusColors.get('open') ?? '' + }, + { + label: 'Suspended', + percent: completion.suspended / allAmount * 100, + color: StatusColors.get('suspended') ?? '' + }, + { + label: 'Overdue', + percent: completion.overdue / allAmount * 100, + color: StatusColors.get('overdue') ?? '' + }, + ] +} + +export function parseActivity(activity: Activity[]): ChartItem[] { + return activity.map(item => ({ + label: item.day, + value: item.time + })); +} diff --git a/client/src/components/forms/ProjectForm/project-form.scss b/client/src/components/forms/ProjectForm/project-form.scss index 066c46a02ecf2c07e39923472dac72c5299359f8..893fa614e72f9365781637840cc4193bf09daa17 100644 --- a/client/src/components/forms/ProjectForm/project-form.scss +++ b/client/src/components/forms/ProjectForm/project-form.scss @@ -1,16 +1,37 @@ +@use 'styles/mixins.scss'as mx; + .project-form { .color-list { display: flex; flex-wrap: wrap; - margin-right: -20px; + margin-right: -10px; + @include mx.breakpoint(large) { + margin-right: -20px; + } + .color-item { - width: calc(100% / 7 - 20px); - margin-right: 20px; - margin-bottom: 20px; - padding-bottom: calc(100% / 7 - 20px); border-radius: 50%; cursor: pointer; - opacity: 0.7; + opacity: 0.5; + margin-right: 10px; + margin-bottom: 10px; + + width: calc(100% / 2 - 10px); + padding-bottom: calc(100% / 2 - 10px); + + @include mx.breakpoint(medium) { + width: calc(100% / 5 - 10px); + padding-bottom: calc(100% / 5 - 10px); + + } + + @include mx.breakpoint(large) { + margin-right: 20px; + margin-bottom: 20px; + width: calc(100% / 7 - 20px); + padding-bottom: calc(100% / 7 - 20px); + } + &.active { opacity: 1; } diff --git a/client/src/components/forms/UserForm/index.tsx b/client/src/components/forms/UserForm/index.tsx index 898c17898a20b641b0d2ec01451ac47d87f8151e..0687297c818a508c49735f0654f2bca4e63ff34f 100644 --- a/client/src/components/forms/UserForm/index.tsx +++ b/client/src/components/forms/UserForm/index.tsx @@ -5,7 +5,7 @@ import TextInput from 'components/ui/TextInput'; import Button from 'components/ui/Button'; interface Props { - onSubmit?: (name?: string, email?: string,) => void; + onSubmit?: (name?: string, email?: string, avatar?: File) => void; user: User } @@ -21,15 +21,29 @@ function validateEmail(email?: string): string | null { } } +function validateAvatar(avatar?: File): string | null { + const validTypes = ['image/jpg', 'image/png', 'image/gif'] + if (avatar) { + if (validTypes.find((type) => type === avatar.type)) { + return null; + } else { + return 'Only files from type jpg, png or gif are allowed' + } + } else { + return null; + } +} + export default function UserForm({ user, onSubmit }: Props) { const [name, setName] = useState(user.realname); const [email, setEmail] = useState(user.email); + const [avatar, setAvatar] = useState<File>(); const handleSubmit = useCallback(async (e: FormEvent) => { e.preventDefault(); - if (validateEmail(email) === null) { - onSubmit?.(name, email); + if (validateEmail(email) === null || validateAvatar(avatar) === null) { + onSubmit?.(name, email, avatar); } - }, [onSubmit, name, email]); + }, [onSubmit, name, email, avatar]); return ( <form onSubmit={handleSubmit} className="user-form"> <div className="fields"> @@ -49,7 +63,12 @@ export default function UserForm({ user, onSubmit }: Props) { <div className="avatar-upload"> <div className="label">Avatar</div> <label htmlFor="avatar" className="avatar-field"> - <input type="file" id="avatar" name="avatar" /> + <input type="file" id="avatar" name="avatar" onChange={(e) => { + if (e.target.files && e.target.files.length > 0) { + setAvatar(e.target.files[0]) + } + }} /> + {avatar ? 'Selected file: ' + avatar.name : 'Select a file'} </label> </div> </div> diff --git a/client/src/components/forms/UserForm/user-form.scss b/client/src/components/forms/UserForm/user-form.scss index 9e478c136507f6e527dc36398a3409c7da65c678..30b7ff3e54e6db41b69da172dfc6dd7a42ce94e9 100644 --- a/client/src/components/forms/UserForm/user-form.scss +++ b/client/src/components/forms/UserForm/user-form.scss @@ -26,6 +26,9 @@ .avatar-upload { width: 100%; position: relative; + input { + display: none; + } .label { position: absolute; top: -2px; @@ -42,6 +45,7 @@ height: 80px; margin-bottom: 20px; background: s.$light; + font-size: 18px; } } } \ No newline at end of file diff --git a/client/src/components/graphs/CircularProgress/circular-progress.scss b/client/src/components/graphs/CircularProgress/circular-progress.scss index cc3d4608607367cf1a85d3bf34761ee112143fb6..a2402523196c97f9e626a779f79cbe7ec2d05bf0 100644 --- a/client/src/components/graphs/CircularProgress/circular-progress.scss +++ b/client/src/components/graphs/CircularProgress/circular-progress.scss @@ -42,7 +42,7 @@ circle { fill: none; - stroke: #dcdcdc; + stroke: #F3F3F3; stroke-width: 8; } } diff --git a/client/src/components/helpers/Filter/filter.scss b/client/src/components/helpers/Filter/filter.scss index ea813ad59c9f22986573ffe415fd98bf25608edf..adc3182edb4bcb02034a2c36822954d22b41737f 100644 --- a/client/src/components/helpers/Filter/filter.scss +++ b/client/src/components/helpers/Filter/filter.scss @@ -32,7 +32,7 @@ .tag-item { margin: 10px; cursor: pointer; - opacity: 0.75; + opacity: 0.5; &.active { opacity: 1; } diff --git a/client/src/components/layout/DetailGrid/detail-grid.scss b/client/src/components/layout/DetailGrid/detail-grid.scss index 07a53db5e40184d79e4c9694d09578d41e706531..04dbb15cbdf26bd5ef019e6b78d162bf5b93386c 100644 --- a/client/src/components/layout/DetailGrid/detail-grid.scss +++ b/client/src/components/layout/DetailGrid/detail-grid.scss @@ -5,17 +5,17 @@ flex-wrap: wrap; margin: -12px; @include mx.breakpoint(medium) { - margin: -16px; + margin: -10px; } .box-container { margin: 12px; width: calc(50% - 24px); @include mx.breakpoint(medium) { - width: calc(25% - 32px); - margin: 16px; + width: calc(33.33% - 20px); + margin: 10px; } @include mx.breakpoint(large) { - width: calc(20% - 32px); + width: calc(20% - 20px); } } } diff --git a/client/src/components/layout/ProjectGrid/index.tsx b/client/src/components/layout/ProjectGrid/index.tsx index 41f845cd83d5b519a9fc785ec8f807a93cafd021..7719310efa9cf7cb9c72bd6b57cb6db6d169ee4b 100644 --- a/client/src/components/layout/ProjectGrid/index.tsx +++ b/client/src/components/layout/ProjectGrid/index.tsx @@ -8,6 +8,7 @@ interface Props { } export default function ProjectGrid({ projects }: Props) { + let counter = 0; return ( <div className="project-grid"> <div className="add-project project"> @@ -16,10 +17,12 @@ export default function ProjectGrid({ projects }: Props) { </Link> </div> { - projects.map(project => ( - <Project key={project.id} project={project} /> - )) - } + projects.map(project => { + counter++; + return <Project key={project.id} project={project} large={(counter - 1) % 5 === 0 && projects.length - 3 >= counter} /> + } + + )} </div > ) } \ No newline at end of file diff --git a/client/src/components/layout/ProjectGrid/project-grid.scss b/client/src/components/layout/ProjectGrid/project-grid.scss index 741ba307f1b1d6a0af2bea04a826dc2692749376..40c1197aa8ac5b7454bb420648134b904c12c5ee 100644 --- a/client/src/components/layout/ProjectGrid/project-grid.scss +++ b/client/src/components/layout/ProjectGrid/project-grid.scss @@ -7,47 +7,31 @@ grid-auto-rows: max-content; grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); + gap: 25px; @include mx.breakpoint(medium) { grid-template-columns: repeat(4, 1fr); + gap: 30px; } - gap: 20px; .add-project { font-size: 64px; font-weight: s.$weight-semi-bold; cursor: pointer; + + a { + color: s.$body-color; + } } .project { width: 100%; padding-bottom: 100%; - @include mx.breakpoint-down(large) { - &:nth-child(5n-3) { - grid-row: span 2; - height: 100%; - - .details { - display: block; - } - } - } - - @include mx.breakpoint(large) { - &:nth-child(8n - 4) { - grid-row: span 2; - height: 100%; - - .details { - display: block; - } - } - } - - &:last-child { - height: 0; + &.large { + grid-row: span 2; + height: 100%; } } diff --git a/client/src/components/ui/AssigneeList/assignee-list.scss b/client/src/components/ui/AssigneeList/assignee-list.scss index a53386af22dbebcd63edf0f82f34122c1e566794..0a639694e8349604f48719b90ffc8f4b157b459a 100644 --- a/client/src/components/ui/AssigneeList/assignee-list.scss +++ b/client/src/components/ui/AssigneeList/assignee-list.scss @@ -2,12 +2,16 @@ .assignee-list { display: flex; + margin-left: 8px; &:hover { .assignee { margin-left: 0; } } + .tooltip { + margin-left: -8px; + } .assignee, .avatar { diff --git a/client/src/components/ui/Button/button.scss b/client/src/components/ui/Button/button.scss index 56b6232d1c062b844e7510b1ced34603dbe31e7d..aaece48879b2e341d7b4617870b2efde40a46ce4 100644 --- a/client/src/components/ui/Button/button.scss +++ b/client/src/components/ui/Button/button.scss @@ -6,7 +6,7 @@ font-size: fn.toRem(18); font-weight: s.$weight-bold; background: s.$linear-gradient; - box-shadow: 0px 5px 15px rgba(s.$primary, 0.1); + box-shadow: 0px 5px 15px rgba(s.$black, 0.1); border-radius: 25px; display: inline-block; color: s.$white; @@ -21,19 +21,20 @@ &:hover, &:focus { - box-shadow: 0px 10px 25px rgba(s.$primary, 0.25); + box-shadow: 0px 10px 25px rgba(s.$black, 0.15); cursor: pointer; color: s.$white; transform: translateY(-5%); + transform-origin: top center; } &:active { - transform: scale(0.9); + transform: scale(0.99); } &.dark { background: s.$primary-dark; - box-shadow: 0px 5px 15px rgba(s.$primary-dark, 0.1); + box-shadow: 0px 5px 15px rgba(s.$black, 0.1); } &.hollow { diff --git a/client/src/components/ui/Project/index.tsx b/client/src/components/ui/Project/index.tsx index d0ab1a673dd6a48ef0fb495ff9171a9455476c48..ed1d3893f678b9a59b9f4be0986620dd27f70de6 100644 --- a/client/src/components/ui/Project/index.tsx +++ b/client/src/components/ui/Project/index.tsx @@ -8,38 +8,47 @@ import { Link } from 'react-router-dom'; import { useEffect, useState } from 'react'; import { Completion } from 'adapters/util'; import LoadingScreen from '../LoadingScreen'; +import Tag from '../Tag'; +import { StatusColors } from 'adapters/project'; export interface ProjectProps { project: IProject + large?: boolean } -export default function Project({ project }: ProjectProps) { +export default function Project({ project, large }: ProjectProps) { const [assignees, setAssignees] = useState<AssignedUser[]>([]); - const [completion, setCOmpletion] = useState<Completion>(); + const [completion, setCompletion] = useState<Completion>(); + useEffect(() => { getProjectAssignees(project.id).then((assignee) => setAssignees(assignee)) - getProjectCompletion(project.id).then((completion) => setCOmpletion(completion)); + getProjectCompletion(project.id).then((completion) => setCompletion(completion)); }, [project]); return ( - <Link to={'/projects/' + project.id} className="project"> + <Link to={'/projects/' + project.id} className={'project ' + (large ? 'large' : '')}> + <div className="status"> + <Tag label={project.status} color={StatusColors.get(project.status)} /> + </div> <div className="content"> { completion ? ( - <CircularProgress percent={completion.closed / (completion.sum ?? 1) * 100 } color={project.color} /> + <CircularProgress percent={completion.closed / (completion.sum ?? 1) * 100} color={project.color} /> ) : ( <LoadingScreen /> ) } <div className="title">{project.name}</div> - <div className="details"> - {project.deadline && ( - <div className="range">{project.deadline.getDate()}</div> - )} - <AssigneeList assignees={assignees} max={3} /> - </div> + { + large && + <div className="details"> + {project.deadline && ( + <div className="deadline">{project.deadline.toUTCString()}</div> + )} + <AssigneeList assignees={assignees} max={3} /> + </div> + } </div> - </Link> ); } diff --git a/client/src/components/ui/Project/project.scss b/client/src/components/ui/Project/project.scss index 99c0002e7e179c3d6e1039a50c4e56f9cac0f3c2..0458cfa38505233529eaad0b3a5a96543a196c5a 100644 --- a/client/src/components/ui/Project/project.scss +++ b/client/src/components/ui/Project/project.scss @@ -1,4 +1,5 @@ @use 'styles/settings'as s; +@use 'styles/mixins'as mx; .project { border-radius: 10px; @@ -15,6 +16,15 @@ box-shadow: 0px 5px 25px rgba(0, 0, 0, 0.1); } + .circular-progress { + justify-content: center; + + svg { + height: auto; + width: 50%; + } + } + .content { display: flex; padding: 20px; @@ -29,10 +39,14 @@ } .title { - margin-top: 10px; + margin-top: 15px; line-height: 1.4; - font-size: 14px; + font-size: 16px; font-weight: s.$weight-bold; + + @include mx.breakpoint(large) { + font-size: 20px; + } } @@ -42,12 +56,25 @@ line-height: 1.8; } + .status { + position: absolute; + top: 0; + left: 50%; + transform: translate(-50%, -50%); + font-size: 12px; + } + .details { - margin-top: 10px; - display: none; + margin-top: 20px; + display: flex; + justify-content: center; + align-items: center; + padding: 10px; + flex-direction: column; + text-align: center; - .range { - margin-bottom: 20px; + .deadline { + margin-bottom: 10px; } font-size: 14px; diff --git a/client/src/components/ui/Tag/index.tsx b/client/src/components/ui/Tag/index.tsx index 5c0261a9c4b955fecab85567dd58ee35cb0b34c0..722f2024ca9ca20705354dc5c7a73134640413fd 100644 --- a/client/src/components/ui/Tag/index.tsx +++ b/client/src/components/ui/Tag/index.tsx @@ -8,7 +8,7 @@ interface Props { export default function Tag({ label, icon, color }: Props) { return ( - <span className={'tag ' + (color ? 'bg-gradient-' + color : '')}> + <span className={'tag ' + (color ? 'bg-gradient-horizontal-' + color : '')}> {icon && ( <i className="icon material-icons"> {icon} diff --git a/client/src/components/ui/Tag/tag.scss b/client/src/components/ui/Tag/tag.scss index 1a6e1023a6d39882816c6c76ca4172081f1dca6b..2b0f0ab18a1de87f5eaa7535c7f0c775dce1a184 100644 --- a/client/src/components/ui/Tag/tag.scss +++ b/client/src/components/ui/Tag/tag.scss @@ -16,7 +16,7 @@ } &:not([class*='bg-gradient']) { - background: s.$linear-gradient; + background: s.$linear-gradient-horizontal; } } \ No newline at end of file diff --git a/client/src/components/ui/Task/task.scss b/client/src/components/ui/Task/task.scss index 5aaca98d4f686495f6673377f51df27bbfab12a0..5f42087dd2451bd51473a8ed4166b7b409f14a0c 100644 --- a/client/src/components/ui/Task/task.scss +++ b/client/src/components/ui/Task/task.scss @@ -16,7 +16,7 @@ box-shadow: 0 5px 30px rgba(s.$black, 0.15); transform: translateY(-5px); - .project-indicator { + .indicator { height: 40%; } } diff --git a/client/src/components/ui/Tooltip/tooltip.scss b/client/src/components/ui/Tooltip/tooltip.scss index 14e3cabb0cb1cbd93a4f129ef1796fc05789b124..a2673ddedf74ba3053ca18a247a2d961384f507a 100644 --- a/client/src/components/ui/Tooltip/tooltip.scss +++ b/client/src/components/ui/Tooltip/tooltip.scss @@ -3,6 +3,7 @@ .tooltip-container { position: relative; z-index: 20000; + text-align: center; &:hover { .tooltip { diff --git a/client/src/config.ts b/client/src/config.ts index f7959fd9c4c74fbb54ca2116f8e95c33efdcde17..a794e21e22ce31dfe62034bf3299d55bdc7f31fb 100644 --- a/client/src/config.ts +++ b/client/src/config.ts @@ -1,4 +1,3 @@ - export let apiRoot: string; if (process.env.NODE_ENV === 'production') { diff --git a/client/src/index.scss b/client/src/index.scss index 6fb12d6dc359a9fdea9b9f5728ffc46e3542412b..fe2baa3f721297354b26253c405af78a91647630 100644 --- a/client/src/index.scss +++ b/client/src/index.scss @@ -68,14 +68,15 @@ h1 { &.underlined { position: relative; display: inline-block; + z-index: 1; - &:after { + &:before { content: ' '; position: absolute; right: -7px; bottom: 0; width: 90px; - background: rgba(s.$secondary, .5); + background: rgba(s.$secondary, 0.5); height: 20px; z-index: -1; @@ -162,6 +163,9 @@ $color in s.$themeLightMap { background: linear-gradient(to bottom, $color 0%, fn.getFrom(s.$themeDarkMap, $key) 100%); } + .bg-gradient-horizontal-#{$key} { + background: linear-gradient(to left, $color 0%, fn.getFrom(s.$themeDarkMap, $key) 100%); + } .theme-#{$key} { --primary: #{$color}; --primary-dark: #{fn.getFrom(s.$themeDarkMap, $key)}; diff --git a/client/src/pages/Projects/ProjectDetail/ProjectDetails/index.tsx b/client/src/pages/Projects/ProjectDetail/ProjectDetails/index.tsx index ca48216016379c3ede87f504c729181943fccb4e..e6ea137ff34f8e7bc5de8754a9b0ea7c9eab708e 100644 --- a/client/src/pages/Projects/ProjectDetail/ProjectDetails/index.tsx +++ b/client/src/pages/Projects/ProjectDetail/ProjectDetails/index.tsx @@ -6,6 +6,7 @@ import { getProjectActivity, Project } from 'adapters/project'; import { useEffect, useState } from 'react'; import { getTeam } from 'adapters/team'; import LoadingScreen from 'components/ui/LoadingScreen'; +import { parseActivity } from 'adapters/util'; interface Props { project: Project @@ -19,14 +20,7 @@ export default function ProjectDetails({ project }: Props) { project.teams.forEach(teamId => { getTeam(teamId).then((team) => setTeams(prev => [...prev, team.name])); }); - getProjectActivity(project.id).then((a) => { - setActivity(a.map(item => { - return { - label: item.day, - value: item.time - } - })); - }) + getProjectActivity(project.id).then((a) => setActivity(parseActivity(a))) }, [project]); let details = [{ diff --git a/client/src/pages/Settings/index.tsx b/client/src/pages/Settings/index.tsx index 3097213b51fc04e84f5a6efcaceb4f7a46da39dc..e9b4e954e09206b467651b1fc031ffa802c4ab62 100644 --- a/client/src/pages/Settings/index.tsx +++ b/client/src/pages/Settings/index.tsx @@ -1,6 +1,6 @@ import './settings.scss'; import { useCallback, useEffect, useState } from 'react'; -import { getCurrentUser, updateUser, User } from 'adapters/user'; +import { getCurrentUser, updateUser, updateUserImage, User } from 'adapters/user'; import LoadingScreen from 'components/ui/LoadingScreen'; import UserForm from 'components/forms/UserForm'; import { useHistory } from 'react-router'; @@ -14,9 +14,12 @@ export default function Settings() { }, []); - const handleSubmit = useCallback(async (name?: string, email?: string) => { + const handleSubmit = useCallback(async (name?: string, email?: string, avatar?: File) => { try { if (user && updateUser({realname: name, email })) { + if(avatar) { + updateUserImage(avatar); + } history.push('/tasks'); } } catch (e) { diff --git a/client/src/pages/Stats/index.tsx b/client/src/pages/Stats/index.tsx index 0ff1a12a0dd2bbd5bce2a36f8829831916dec216..e663e9465ea6f9fcd54d5c9763fa52f167b51b32 100644 --- a/client/src/pages/Stats/index.tsx +++ b/client/src/pages/Stats/index.tsx @@ -1,11 +1,35 @@ import './stats.scss'; +import LoadingScreen from 'components/ui/LoadingScreen'; +import { useEffect, useState } from 'react'; +import { getUserActivity, getUserCompletion } from 'adapters/user'; +import { CompletionProps } from 'components/ui/Completion'; +import { parseActivity, parseCompletion } from 'adapters/util'; +import CompletionGrid from 'components/layout/CompletionGrid'; +import BarChart, { ChartItem } from 'components/graphs/BarChart'; export default function Tasks() { - return ( - <div className="stats-page"> - <div className="content-container"> - <h1 className="underlined">Stats</h1> + const [completions, setCompletions] = useState<CompletionProps[]>(); + const [activity, setActivity] = useState<ChartItem[]>(); + + useEffect(() => { + getUserCompletion().then((completion) => setCompletions(parseCompletion(completion))); + getUserActivity().then((a) => setActivity(parseActivity(a))) + }, []); + + + + if (completions && activity) { + return ( + <div className="stats-page"> + <div className="content-container"> + <h1 className="underlined">Stats</h1> + <h2>Activity</h2> + <BarChart data={activity} /> + <h2>Completion</h2> + <CompletionGrid items={completions} /> + </div> </div> - </div> - ); + ); + } + return <LoadingScreen /> } diff --git a/client/src/pages/Stats/stats.scss b/client/src/pages/Stats/stats.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b61801794329b3dd391a5669f67ea57661489119 100644 --- a/client/src/pages/Stats/stats.scss +++ b/client/src/pages/Stats/stats.scss @@ -0,0 +1,5 @@ +.stats-page { + h2 { + margin-top: 30px; + } +} \ No newline at end of file diff --git a/client/src/pages/Tasks/TaskStart/index.tsx b/client/src/pages/Tasks/TaskStart/index.tsx index 9489f19fa75d983e065f88f0dc63c9f27e4ce4b3..ee07faa285f0bd6b080076b3071253a79029e182 100644 --- a/client/src/pages/Tasks/TaskStart/index.tsx +++ b/client/src/pages/Tasks/TaskStart/index.tsx @@ -1,7 +1,6 @@ import './task-start.scss'; import Tag from 'components/ui/Tag'; import DetailGrid from 'components/layout/DetailGrid'; -import ButtonLink from 'components/navigation/ButtonLink'; import { useHistory, useParams } from 'react-router'; import { useEffect, useState } from 'react'; import { getTask, StatusColors, Task } from 'adapters/task'; diff --git a/client/src/pages/Tasks/index.tsx b/client/src/pages/Tasks/index.tsx index f876bf3694b3427247a3fde020f2694830246a01..0e91aea1d686ad0ca192e977d6d9920d3166ca01 100644 --- a/client/src/pages/Tasks/index.tsx +++ b/client/src/pages/Tasks/index.tsx @@ -32,7 +32,8 @@ export default function Tasks() { { tasks ? ( <> - <p>Hey Daniel, you have <strong>{tasks.length} tasks</strong> for today.</p> + <p>Hey Daniel, you have <strong>{tasks.length} {tasks.length > 1 ? 'tasks' : 'task'} + </strong> for today.</p> <section className="tasks-container"> <h2>Today</h2> <div className="task-group"> @@ -47,10 +48,10 @@ export default function Tasks() { </div> </section> </> - ) : - ( - <div>No open tasks found</div> - ) + ) : + ( + <div>No open tasks found</div> + ) } </main> </div> diff --git a/client/src/pages/Teams/TeamsStats/index.tsx b/client/src/pages/Teams/TeamsStats/index.tsx index f5c999a6275d768d46e3025812d928806ddff005..e0100b9be6631972cc328910cd5406f899413d29 100644 --- a/client/src/pages/Teams/TeamsStats/index.tsx +++ b/client/src/pages/Teams/TeamsStats/index.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; import { getTeamActivity, getTeamCompletion } from 'adapters/team'; import LoadingScreen from 'components/ui/LoadingScreen'; import { CompletionProps } from 'components/ui/Completion'; -import { StatusColors } from 'adapters/task'; +import { parseActivity, parseCompletion } from 'adapters/util'; interface Props { teamId: string; @@ -18,37 +18,8 @@ export default function TeamsStats({ teamId }: Props) { useEffect(() => { - getTeamActivity(teamId).then((a) => { - setActivity(a.map(item => ({ - label: item.day, - value: item.time - }))); - }); - getTeamCompletion(teamId).then((comp) => { - const allAmount = comp.sum ?? 1; - setCompletions([ - { - label: 'Closed', - percent: comp.closed / allAmount * 100, - color: StatusColors.get('closed') ?? '' - }, - { - label: 'Open', - percent: comp.open / allAmount * 100, - color: StatusColors.get('open') ?? '' - }, - { - label: 'Suspended', - percent: comp.suspended / allAmount * 100, - color: StatusColors.get('suspended') ?? '' - }, - { - label: 'Overdue', - percent: comp.overdue / allAmount * 100, - color: StatusColors.get('overdue') ?? '' - }, - ]); - }) + getTeamActivity(teamId).then((a) => setActivity(parseActivity(a))); + getTeamCompletion(teamId).then((comp) => setCompletions(parseCompletion(comp))) }, [teamId]) return ( <section className="teams-stats-section"> diff --git a/client/src/pages/Teams/index.tsx b/client/src/pages/Teams/index.tsx index 088a7276d8cc924ae195be2a4a7cf95b84c8cb73..958810726a9f4453c7414f8541e31d512efbf7e2 100644 --- a/client/src/pages/Teams/index.tsx +++ b/client/src/pages/Teams/index.tsx @@ -92,15 +92,18 @@ export default function Teams() { <div className="teams-page"> <div className="content-container"> <h1 className="underlined">Teams</h1> + <div className="description-container"> + <p>Here you can see information about your teams, as well as its stats and members.</p> + </div> { - allTeams && ( + allTeams ? ( <Dropdown items={pageLinks}> <h2>{currentTeam?.name}</h2> <span className="material-icons icon"> expand_more </span> </Dropdown> - ) + ) : <LoadingScreen /> } { details ? ( @@ -109,16 +112,17 @@ export default function Teams() { <LoadingScreen /> ) } - - <ButtonLink href={'/teams/' + currentTeam?.id + '/edit'} className="expanded"> - Edit - </ButtonLink> - { - allTeams && allTeams.length > 1 && - <Button className="expanded dark" onClick={leaveCurrentTeam}> - Leave Team - </Button> - } + <div className="buttons"> + <ButtonLink href={'/teams/' + currentTeam?.id + '/edit'} className="expanded"> + Edit + </ButtonLink> + { + allTeams && allTeams.length > 1 && ( + <Button className="expanded dark" onClick={leaveCurrentTeam}> + Leave Team + </Button>) + } + </div> { tabs ? ( <Tabs tabs={tabs} /> diff --git a/client/src/pages/Teams/teams.scss b/client/src/pages/Teams/teams.scss index fab67b03b5ce56ede4a6a5a24878cf57d43184ba..c066f145bf66305de9153b629e7f7669d87c9be3 100644 --- a/client/src/pages/Teams/teams.scss +++ b/client/src/pages/Teams/teams.scss @@ -5,10 +5,16 @@ margin: 0; } } - .button { + .buttons { margin: 25px 0; } + .button { + margin: 5px 0; + } .detail-grid { margin-top: 10px; } + .description-container { + margin-bottom: 40px; + } } diff --git a/client/src/styles/settings.scss b/client/src/styles/settings.scss index fb92ed280892a3e098a3db06a288aaaab441f813..81a8f0547d6fd4b2e0b5101fc878812ac0a0fbf4 100644 --- a/client/src/styles/settings.scss +++ b/client/src/styles/settings.scss @@ -4,7 +4,7 @@ $primary: var(--primary); $primary-dark: var(--primary-dark); $secondary: #7DEFFF; $red: #E51C4A; -$light: rgba(0, 0, 0, 0.025); +$light: #F9F9F9; $dark: #180923; $light-gray: #F8F8F8;