From c4c6d738a5ba5438e09116953bea15ae030e4df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arkadiusz=20Dzi=C4=99giel?= Date: Mon, 12 Aug 2024 16:29:50 +0000 Subject: [PATCH] feat: Colorize due date and make it toggleable (#845) --- client/src/components/Card/Card.jsx | 18 ++- client/src/components/CardModal/CardModal.jsx | 20 +++- client/src/components/DueDate/DueDate.jsx | 107 +++++++++++++----- .../components/DueDate/DueDate.module.scss | 53 +++++++++ client/src/containers/CardContainer.js | 7 +- client/src/containers/CardModalContainer.js | 2 + client/src/models/Card.js | 4 + server/api/controllers/cards/create.js | 12 +- server/api/controllers/cards/update.js | 4 + server/api/helpers/cards/duplicate-one.js | 1 + server/api/models/Card.js | 5 + .../20240812065305_add_due_completion.js.js | 12 ++ 12 files changed, 207 insertions(+), 38 deletions(-) create mode 100644 server/db/migrations/20240812065305_add_due_completion.js.js diff --git a/client/src/components/Card/Card.jsx b/client/src/components/Card/Card.jsx index 4adddbc5..45e91e5d 100755 --- a/client/src/components/Card/Card.jsx +++ b/client/src/components/Card/Card.jsx @@ -24,6 +24,7 @@ const Card = React.memo( index, name, dueDate, + dueCompleted, stopwatch, coverUrl, boardId, @@ -81,6 +82,15 @@ const Card = React.memo( [onUpdate], ); + const handleDueDateCompletionUpdate = useCallback( + (dueDateCompleted) => { + onUpdate({ + dueDateCompleted, + }); + }, + [onUpdate], + ); + const handleNameEdit = useCallback(() => { nameEdit.current.open(); }, []); @@ -120,7 +130,12 @@ const Card = React.memo( )} {dueDate && ( - + )} {stopwatch && ( @@ -221,6 +236,7 @@ Card.propTypes = { index: PropTypes.number.isRequired, name: PropTypes.string.isRequired, dueDate: PropTypes.instanceOf(Date), + dueCompleted: PropTypes.bool.isRequired, stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types coverUrl: PropTypes.string, boardId: PropTypes.string.isRequired, diff --git a/client/src/components/CardModal/CardModal.jsx b/client/src/components/CardModal/CardModal.jsx index 91e89474..889e6d7c 100755 --- a/client/src/components/CardModal/CardModal.jsx +++ b/client/src/components/CardModal/CardModal.jsx @@ -32,6 +32,7 @@ const CardModal = React.memo( name, description, dueDate, + dueCompleted, stopwatch, isSubscribed, isActivitiesFetching, @@ -171,6 +172,15 @@ const CardModal = React.memo( onClose(); }, [onClose]); + const handleDueDateCompletion = useCallback( + (completion) => { + onUpdate({ + dueCompleted: completion, + }); + }, + [onUpdate], + ); + const AttachmentAddPopup = usePopup(AttachmentAddStep); const BoardMembershipsPopup = usePopup(BoardMembershipsStep); const LabelsPopup = usePopup(LabelsStep); @@ -303,10 +313,14 @@ const CardModal = React.memo( {canEdit ? ( - + ) : ( - + )} @@ -562,6 +576,7 @@ CardModal.propTypes = { name: PropTypes.string.isRequired, description: PropTypes.string, dueDate: PropTypes.instanceOf(Date), + dueCompleted: PropTypes.bool, stopwatch: PropTypes.object, // eslint-disable-line react/forbid-prop-types isSubscribed: PropTypes.bool.isRequired, isActivitiesFetching: PropTypes.bool.isRequired, @@ -616,6 +631,7 @@ CardModal.propTypes = { CardModal.defaultProps = { description: undefined, dueDate: undefined, + dueCompleted: false, stopwatch: undefined, }; diff --git a/client/src/components/DueDate/DueDate.jsx b/client/src/components/DueDate/DueDate.jsx index 59f0754c..8c4d78d3 100644 --- a/client/src/components/DueDate/DueDate.jsx +++ b/client/src/components/DueDate/DueDate.jsx @@ -1,8 +1,9 @@ import upperFirst from 'lodash/upperFirst'; -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; +import { Checkbox } from 'semantic-ui-react'; import getDateFormat from '../../utils/get-date-format'; @@ -26,50 +27,96 @@ const FULL_DATE_FORMAT_BY_SIZE = { medium: 'fullDateTime', }; -const DueDate = React.memo(({ value, size, isDisabled, onClick }) => { - const [t] = useTranslation(); +const getDueClass = (value) => { + const now = new Date(); + const tomorrow = new Date(now).setDate(now.getDate() + 1); - const dateFormat = getDateFormat( - value, - LONG_DATE_FORMAT_BY_SIZE[size], - FULL_DATE_FORMAT_BY_SIZE[size], - ); + if (now > value) return styles.overdue; + if (tomorrow > value) return styles.soon; + return null; +}; - const contentNode = ( - - {t(`format:${dateFormat}`, { - value, - postProcess: 'formatDate', - })} - - ); +const DueDate = React.memo( + ({ value, completed, size, isDisabled, onClick, onUpdateCompletion }) => { + const [t] = useTranslation(); - return onClick ? ( - - ) : ( - contentNode - ); -}); + const dateFormat = getDateFormat( + value, + LONG_DATE_FORMAT_BY_SIZE[size], + FULL_DATE_FORMAT_BY_SIZE[size], + ); + + const classes = [ + styles.wrapper, + styles[`wrapper${upperFirst(size)}`], + onClick && styles.wrapperHoverable, + completed ? styles.completed : getDueClass(value), + ]; + + const handleToggleChange = useCallback( + (event) => { + event.preventDefault(); + event.stopPropagation(); + if (!isDisabled) onUpdateCompletion(!completed); + }, + [onUpdateCompletion, completed, isDisabled], + ); + + return onClick ? ( +
+ + +
+ ) : ( + + {t(`format:${dateFormat}`, { + value, + postProcess: 'formatDate', + })} + + ); + }, +); DueDate.propTypes = { value: PropTypes.instanceOf(Date).isRequired, size: PropTypes.oneOf(Object.values(SIZES)), isDisabled: PropTypes.bool, + completed: PropTypes.bool, onClick: PropTypes.func, + onUpdateCompletion: PropTypes.func, }; DueDate.defaultProps = { size: SIZES.MEDIUM, isDisabled: false, + completed: false, onClick: undefined, + onUpdateCompletion: undefined, }; export default DueDate; diff --git a/client/src/components/DueDate/DueDate.module.scss b/client/src/components/DueDate/DueDate.module.scss index 45ffa50a..42dbae47 100644 --- a/client/src/components/DueDate/DueDate.module.scss +++ b/client/src/components/DueDate/DueDate.module.scss @@ -25,22 +25,75 @@ color: #17394d; } + .wrapperGroup { + display: flex; + align-items: stretch; + justify-content: stretch; + } + + .overdue { + background: #db2828; + color: #ffffff; + &.wrapperHoverable:hover { + background: #d01919; + color: #ffffff; + } + } + + .soon { + background: #fbbd08; + color: #ffffff; + &.wrapperHoverable:hover { + background: #eaae00; + color: #ffffff; + } + } + + .completed { + background: #21ba45; + color: #ffffff; + &.wrapperHoverable:hover { + background: #16ab39; + color: #ffffff; + } + } + /* Sizes */ .wrapperTiny { font-size: 12px; line-height: 20px; padding: 0px 6px; + &.wrapperCheckbox { + padding-right: 6px; + } } .wrapperSmall { font-size: 12px; line-height: 20px; padding: 2px 6px; + &.wrapperCheckbox { + padding-right: 6px; + } } .wrapperMedium { line-height: 20px; padding: 6px 12px; } + + .wrapperCheckbox { + padding-right: 12px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + cursor: pointer; + } + .checkbox { + display: block; + } + .wrapperButton { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } } diff --git a/client/src/containers/CardContainer.js b/client/src/containers/CardContainer.js index e1745644..7c58e549 100755 --- a/client/src/containers/CardContainer.js +++ b/client/src/containers/CardContainer.js @@ -20,10 +20,8 @@ const makeMapStateToProps = () => { const allLabels = selectors.selectLabelsForCurrentBoard(state); const currentUserMembership = selectors.selectCurrentUserMembershipForCurrentBoard(state); - const { name, dueDate, stopwatch, coverUrl, boardId, listId, isPersisted } = selectCardById( - state, - id, - ); + const { name, dueDate, dueCompleted, stopwatch, coverUrl, boardId, listId, isPersisted } = + selectCardById(state, id); const users = selectUsersByCardId(state, id); const labels = selectLabelsByCardId(state, id); @@ -38,6 +36,7 @@ const makeMapStateToProps = () => { index, name, dueDate, + dueCompleted, stopwatch, coverUrl, boardId, diff --git a/client/src/containers/CardModalContainer.js b/client/src/containers/CardModalContainer.js index a8df6ea0..05ed8a4c 100755 --- a/client/src/containers/CardModalContainer.js +++ b/client/src/containers/CardModalContainer.js @@ -21,6 +21,7 @@ const mapStateToProps = (state) => { name, description, dueDate, + dueCompleted, stopwatch, isSubscribed, isActivitiesFetching, @@ -49,6 +50,7 @@ const mapStateToProps = (state) => { name, description, dueDate, + dueCompleted, stopwatch, isSubscribed, isActivitiesFetching, diff --git a/client/src/models/Card.js b/client/src/models/Card.js index c0066634..f811b3dc 100755 --- a/client/src/models/Card.js +++ b/client/src/models/Card.js @@ -20,6 +20,9 @@ export default class extends BaseModel { relatedName: 'ownCards', }), dueDate: attr(), + dueCompleted: attr({ + getDefault: () => false, + }), stopwatch: attr(), isSubscribed: attr({ getDefault: () => false, @@ -248,6 +251,7 @@ export default class extends BaseModel { 'name', 'description', 'dueDate', + 'dueCompleted', 'stopwatch', ]), ...payload.card, diff --git a/server/api/controllers/cards/create.js b/server/api/controllers/cards/create.js index 33bc6b2d..28285cb9 100755 --- a/server/api/controllers/cards/create.js +++ b/server/api/controllers/cards/create.js @@ -57,6 +57,9 @@ module.exports = { type: 'string', custom: dueDateValidator, }, + dueCompleted: { + type: 'boolean', + }, stopwatch: { type: 'json', custom: stopwatchValidator, @@ -95,7 +98,14 @@ module.exports = { throw Errors.NOT_ENOUGH_RIGHTS; } - const values = _.pick(inputs, ['position', 'name', 'description', 'dueDate', 'stopwatch']); + const values = _.pick(inputs, [ + 'position', + 'name', + 'description', + 'dueDate', + 'dueCompleted', + 'stopwatch', + ]); const card = await sails.helpers.cards.createOne .with({ diff --git a/server/api/controllers/cards/update.js b/server/api/controllers/cards/update.js index d3deaf48..763057a6 100755 --- a/server/api/controllers/cards/update.js +++ b/server/api/controllers/cards/update.js @@ -80,6 +80,9 @@ module.exports = { custom: dueDateValidator, allowNull: true, }, + dueCompleted: { + type: 'boolean', + }, stopwatch: { type: 'json', custom: stopwatchValidator, @@ -173,6 +176,7 @@ module.exports = { 'name', 'description', 'dueDate', + 'dueCompleted', 'stopwatch', 'isSubscribed', ]); diff --git a/server/api/helpers/cards/duplicate-one.js b/server/api/helpers/cards/duplicate-one.js index 94a0d755..3ea30443 100644 --- a/server/api/helpers/cards/duplicate-one.js +++ b/server/api/helpers/cards/duplicate-one.js @@ -77,6 +77,7 @@ module.exports = { 'name', 'description', 'dueDate', + 'dueCompleted', 'stopwatch', ]), ...values, diff --git a/server/api/models/Card.js b/server/api/models/Card.js index e1e7335e..fe6738d6 100755 --- a/server/api/models/Card.js +++ b/server/api/models/Card.js @@ -28,6 +28,11 @@ module.exports = { type: 'ref', columnName: 'due_date', }, + dueCompleted: { + type: 'boolean', + defaultsTo: false, + columnName: 'due_completed', + }, stopwatch: { type: 'json', }, diff --git a/server/db/migrations/20240812065305_add_due_completion.js.js b/server/db/migrations/20240812065305_add_due_completion.js.js new file mode 100644 index 00000000..331782ae --- /dev/null +++ b/server/db/migrations/20240812065305_add_due_completion.js.js @@ -0,0 +1,12 @@ +module.exports.up = async (knex) => + knex.schema.table('card', (table) => { + /* Columns */ + + table.boolean('due_completed').notNullable().defaultTo(false); + }); + +module.exports.down = async (knex) => { + await knex.schema.table('card', (table) => { + table.dropColumn('due_completed'); + }); +};