Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 9366a10

Browse files
committed
Added working days update hint.
1 parent 12a7c71 commit 9366a10

File tree

14 files changed

+337
-35
lines changed

14 files changed

+337
-35
lines changed
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { useEffect, useState } from "react";
2+
import PT from "prop-types";
3+
import cn from "classnames";
4+
import Icon from "../../../assets/images/icon-checkmark-circled.svg";
5+
import styles from "./styles.module.scss";
6+
7+
/**
8+
* Displays an animated checkmark inside circle. After the specified timeout
9+
* the checkmark is faded out and after fade transition ends the onTimeout
10+
* is called.
11+
*
12+
* @param {Object} props component properties
13+
* @param {string} [props.className] class name to be added to root element
14+
* @param {() => void} props.onTimeout
15+
* @param {number} props.timeout timeout milliseconds
16+
* @returns {JSX.Element}
17+
*/
18+
const CheckmarkCircled = ({ className, onTimeout, timeout = 2000 }) => {
19+
const [isAnimated, setIsAnimated] = useState(false);
20+
const [isTimedOut, setIsTimedOut] = useState(false);
21+
22+
useEffect(() => {
23+
setIsAnimated(true);
24+
}, []);
25+
26+
useEffect(() => {
27+
setIsTimedOut(false);
28+
let timeoutId = setTimeout(() => {
29+
timeoutId = 0;
30+
setIsTimedOut(true);
31+
}, Math.max(timeout, /* total CSS animation duration */ 1200));
32+
return () => {
33+
if (timeoutId) {
34+
clearTimeout(timeoutId);
35+
}
36+
};
37+
}, [timeout]);
38+
39+
return (
40+
<span
41+
className={cn(
42+
styles.container,
43+
{ [styles.fadeOut]: isTimedOut },
44+
className
45+
)}
46+
onTransitionEnd={isTimedOut ? onTimeout : null}
47+
>
48+
<Icon
49+
className={cn(styles.checkmark, { [styles.animated]: isAnimated })}
50+
/>
51+
</span>
52+
);
53+
};
54+
55+
CheckmarkCircled.propTypes = {
56+
className: PT.string,
57+
onTimeout: PT.func.isRequired,
58+
timeout: PT.number,
59+
};
60+
61+
export default CheckmarkCircled;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
@import "styles/variables";
2+
3+
.container {
4+
display: inline-block;
5+
width: 30px;
6+
height: 30px;
7+
opacity: 1;
8+
transition: opacity 0.2s ease;
9+
}
10+
11+
.checkmark {
12+
display: block;
13+
width: auto;
14+
height: 100%;
15+
border-radius: 999px;
16+
stroke-width: 2;
17+
stroke: $primary-color;
18+
stroke-miterlimit: 10;
19+
box-shadow: inset 0px 0px 0px $primary-color;
20+
animation-play-state: paused;
21+
animation: /*checkmark-circled-fill 0.4s ease-in-out 0.4s forwards,*/ checkmark-circled-scale
22+
0.3s ease-in-out 0.9s both;
23+
24+
:global(.checkmark__circle) {
25+
stroke-dasharray: 166;
26+
stroke-dashoffset: 166;
27+
stroke-width: 2;
28+
stroke-miterlimit: 10;
29+
stroke: $primary-color;
30+
fill: rgba(255, 255, 255, 0);
31+
animation-play-state: paused;
32+
animation: checkmark-circled-stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1)
33+
forwards;
34+
}
35+
36+
:global(.checkmark__check) {
37+
transform-origin: 50% 50%;
38+
stroke-dasharray: 48;
39+
stroke-dashoffset: 48;
40+
animation-play-state: paused;
41+
animation: checkmark-circled-stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s
42+
forwards;
43+
}
44+
}
45+
46+
.animated {
47+
animation-play-state: running;
48+
49+
:global(.checkmark__circle),
50+
:global(.checkmark__check) {
51+
animation-play-state: running;
52+
}
53+
}
54+
55+
.fadeOut {
56+
opacity: 0;
57+
}
58+
59+
@keyframes checkmark-circled-stroke {
60+
100% {
61+
stroke-dashoffset: 0;
62+
}
63+
}
64+
65+
@keyframes checkmark-circled-scale {
66+
0%,
67+
100% {
68+
transform: none;
69+
}
70+
50% {
71+
transform: scale3d(1.1, 1.1, 1);
72+
}
73+
}
74+
75+
@keyframes checkmark-circled-fill {
76+
100% {
77+
box-shadow: inset 0px 0px 0px 10px $primary-color;
78+
}
79+
}

src/decls/svg.d.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
declare module '*.svg' {
2-
const value: string;
1+
declare module "*.svg" {
2+
const value: import("react").FunctionComponent<
3+
React.SVGAttributes<SVGElement>
4+
>;
35
export default value;
46
}

src/routes/WorkPeriods/components/PeriodItem/index.jsx

+13-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import PT from "prop-types";
44
import cn from "classnames";
55
import debounce from "lodash/debounce";
66
import Checkbox from "components/Checkbox";
7-
import IntegerField from "components/IntegerField";
87
import ProjectName from "components/ProjectName";
98
import PaymentError from "../PaymentError";
109
import PaymentStatus from "../PaymentStatus";
@@ -13,6 +12,7 @@ import PeriodDetails from "../PeriodDetails";
1312
import { PAYMENT_STATUS } from "constants/workPeriods";
1413
import {
1514
setWorkPeriodWorkingDays,
15+
toggleWorkingDaysUpdated,
1616
toggleWorkPeriod,
1717
} from "store/actions/workPeriods";
1818
import {
@@ -23,6 +23,7 @@ import { useUpdateEffect } from "utils/hooks";
2323
import { formatUserHandleLink, formatWeeklyRate } from "utils/formatters";
2424
import { stopPropagation } from "utils/misc";
2525
import styles from "./styles.module.scss";
26+
import PeriodWorkingDays from "../PeriodWorkingDays";
2627

2728
/**
2829
* Displays the working period data row to be used in PeriodList component.
@@ -57,6 +58,10 @@ const PeriodItem = ({
5758
dispatch(toggleWorkPeriodDetails(item));
5859
}, [dispatch, item]);
5960

61+
const onWorkingDaysUpdateHintTimeout = useCallback(() => {
62+
dispatch(toggleWorkingDaysUpdated(item.id, false));
63+
}, [dispatch, item.id]);
64+
6065
const onWorkingDaysChange = useCallback(
6166
(daysWorked) => {
6267
dispatch(setWorkPeriodWorkingDays(item.id, daysWorked));
@@ -141,19 +146,19 @@ const PeriodItem = ({
141146
<PaymentStatus status={data.paymentStatus} />
142147
</td>
143148
<td className={styles.daysWorked}>
144-
<IntegerField
145-
className={styles.daysWorkedControl}
149+
<PeriodWorkingDays
150+
updateHintTimeout={2000}
151+
controlName={`wp_wrk_days_${item.id}`}
152+
data={data}
146153
isDisabled={isDisabled}
147-
name={`wp_wrk_days_${item.id}`}
148-
onChange={onWorkingDaysChange}
149-
maxValue={5}
150-
minValue={data.daysPaid}
151-
value={data.daysWorked}
154+
onWorkingDaysChange={onWorkingDaysChange}
155+
onWorkingDaysUpdateHintTimeout={onWorkingDaysUpdateHintTimeout}
152156
/>
153157
</td>
154158
</tr>
155159
{details && (
156160
<PeriodDetails
161+
className="period-details"
157162
details={details}
158163
isDisabled={isDisabled}
159164
isFailed={isFailed}

src/routes/WorkPeriods/components/PeriodItem/styles.module.scss

+17-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
.container {
55
> td {
66
padding-left: 15px;
7-
padding-right: 18px;
7+
padding-right: 15px;
88
background: #fff;
99
}
1010

@@ -40,6 +40,20 @@
4040
}
4141
}
4242

43+
:global(.period-details) {
44+
+ .container.hasDetails {
45+
> td {
46+
&.toggle {
47+
padding-top: 12px;
48+
}
49+
50+
&.daysWorked {
51+
padding-top: 5px;
52+
}
53+
}
54+
}
55+
}
56+
4357
td.toggle {
4458
padding: 12px 18px 12px 15px;
4559
line-height: 15px;
@@ -67,6 +81,8 @@ td.teamName {
6781

6882
td.startDate,
6983
td.endDate {
84+
padding-left: 10px;
85+
padding-right: 10px;
7086
white-space: nowrap;
7187
}
7288

@@ -90,7 +106,3 @@ td.paymentTotal {
90106
td.daysWorked {
91107
padding: 5px 10px;
92108
}
93-
94-
.daysWorkedControl {
95-
width: 100px;
96-
}

src/routes/WorkPeriods/components/PeriodListHead/index.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ const PeriodListHead = () => {
7373
const HEAD_CELLS = [
7474
{ label: "Topcoder Handle", id: SORT_BY.USER_HANDLE },
7575
{ label: "Team Name", id: SORT_BY.TEAM_NAME, disableSort: true },
76-
{ label: "Start Date", id: SORT_BY.START_DATE },
77-
{ label: "End Date", id: SORT_BY.END_DATE },
76+
{ label: "Start Date", id: SORT_BY.START_DATE, className: "startDate" },
77+
{ label: "End Date", id: SORT_BY.END_DATE, className: "endDate" },
7878
{ label: "Weekly Rate", id: SORT_BY.WEEKLY_RATE, className: "weeklyRate" },
7979
{ label: "Total Paid", id: SORT_BY.PAYMENT_TOTAL, className: "totalPaid" },
8080
{ label: "Status", id: SORT_BY.PAYMENT_STATUS },

src/routes/WorkPeriods/components/PeriodListHead/styles.module.scss

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@import "styles/mixins";
22

33
.container {
4-
th {
4+
> th {
55
text-align: left;
66
background: #f4f4f4;
77

@@ -34,7 +34,7 @@
3434

3535
&:last-child {
3636
.colHead {
37-
padding: 12px 10px;
37+
padding: 12px 10px 12px 50px;
3838

3939
&::before {
4040
right: -20px;
@@ -43,6 +43,12 @@
4343
}
4444
}
4545

46+
:global(.startDate),
47+
:global(.endDate) {
48+
padding-left: 10px;
49+
padding-right: 10px;
50+
}
51+
4652
:global(.weeklyRate),
4753
:global(.totalPaid) {
4854
justify-content: flex-end;
@@ -54,7 +60,7 @@
5460
display: flex;
5561
justify-content: flex-start;
5662
align-items: center;
57-
padding: 12px 17px;
63+
padding: 12px 15px;
5864
height: 40px;
5965
}
6066

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from "react";
2+
import PT from "prop-types";
3+
import cn from "classnames";
4+
import IntegerField from "components/IntegerField";
5+
import IconCheckmarkCircled from "components/Icons/CheckmarkCircled";
6+
import styles from "./styles.module.scss";
7+
8+
/**
9+
* Displays working days input field with an icon hinting about the update.
10+
*
11+
* @param {Object} props component properties
12+
* @param {string} [props.className] class name to be added to root element
13+
* @param {string} props.controlName working days input control name
14+
* @param {Object} props.data working period data object
15+
* @param {boolean} props.isDisabled whether the input field should be disabled
16+
* @param {(v: number) => void} props.onWorkingDaysChange function called when
17+
* working days change
18+
* @param {() => void} props.onWorkingDaysUpdateHintTimeout function called when
19+
* update hint icon has finished its animation
20+
* @param {number} [props.updateHintTimeout] timeout in milliseconds for update
21+
* hint icon
22+
* @returns {JSX.Element}
23+
*/
24+
const PeriodWorkingDays = ({
25+
className,
26+
controlName,
27+
data,
28+
isDisabled,
29+
onWorkingDaysChange,
30+
onWorkingDaysUpdateHintTimeout,
31+
updateHintTimeout = 2000,
32+
}) => (
33+
<div className={cn(styles.container, className)}>
34+
<span className={styles.iconPlaceholder}>
35+
{data.daysWorkedIsUpdated && (
36+
<IconCheckmarkCircled
37+
className={styles.checkmarkIcon}
38+
onTimeout={onWorkingDaysUpdateHintTimeout}
39+
timeout={updateHintTimeout}
40+
/>
41+
)}
42+
</span>
43+
<IntegerField
44+
className={styles.daysWorkedControl}
45+
isDisabled={isDisabled}
46+
name={controlName}
47+
onChange={onWorkingDaysChange}
48+
maxValue={5}
49+
minValue={data.daysPaid}
50+
value={data.daysWorked}
51+
/>
52+
</div>
53+
);
54+
55+
PeriodWorkingDays.propTypes = {
56+
className: PT.string,
57+
controlName: PT.string.isRequired,
58+
data: PT.shape({
59+
daysPaid: PT.number.isRequired,
60+
daysWorked: PT.number.isRequired,
61+
daysWorkedIsUpdated: PT.bool.isRequired,
62+
}).isRequired,
63+
isDisabled: PT.bool.isRequired,
64+
onWorkingDaysChange: PT.func.isRequired,
65+
onWorkingDaysUpdateHintTimeout: PT.func.isRequired,
66+
updateHintTimeout: PT.number,
67+
};
68+
69+
export default PeriodWorkingDays;

0 commit comments

Comments
 (0)