feat: drop react-datepicker and change due date labels to time ago

This commit is contained in:
2026-07-20 12:08:19 -03:00
parent 891c526363
commit 27a8cef216
15 changed files with 61 additions and 275 deletions
+8
View File
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## 2026-07-20
### Changed
- Labels in tasks due date to use the time ago format.
### Removed
- React Date Picker dependency in favour of regular browser input date UI.
## app-v2026.07.01.140 - 2026-07-01
### Added
-169
View File
@@ -20,7 +20,6 @@
"react-bootstrap": "^2.10.10",
"react-bootstrap-icons": "^1.11.6",
"react-charts": "^3.0.0-beta.57",
"react-datepicker": "^9.1.0",
"react-dom": "^19.2.7",
"react-i18next": "^17.0.8",
"react-markdown": "^10.1.0",
@@ -705,59 +704,6 @@
}
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/react": {
"version": "0.27.19",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz",
"integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.8",
"@floating-ui/utils": "^0.2.11",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
"integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.6"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -1108,9 +1054,6 @@
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1131,9 +1074,6 @@
"cpu": [
"arm"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1154,9 +1094,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1177,9 +1114,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1200,9 +1134,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1223,9 +1154,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1476,9 +1404,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1495,9 +1420,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1514,9 +1436,6 @@
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1533,9 +1452,6 @@
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1552,9 +1468,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1571,9 +1484,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2569,9 +2479,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2586,9 +2493,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2603,9 +2507,6 @@
"loong64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2620,9 +2521,6 @@
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2637,9 +2535,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2654,9 +2549,6 @@
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2671,9 +2563,6 @@
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2688,9 +2577,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2705,9 +2591,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -2722,9 +2605,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -4191,16 +4071,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz",
"integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dayjs": {
"version": "1.11.21",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
@@ -7248,9 +7118,6 @@
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7271,9 +7138,6 @@
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7294,9 +7158,6 @@
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7317,9 +7178,6 @@
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9334,27 +9192,6 @@
"@types/react": "^17.0.0"
}
},
"node_modules/react-datepicker": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz",
"integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.27.15",
"clsx": "^2.1.1",
"date-fns": "^4.1.0"
},
"peerDependencies": {
"date-fns-tz": "^3.0.0",
"react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"date-fns-tz": {
"optional": true
}
}
},
"node_modules/react-dom": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
@@ -10653,12 +10490,6 @@
"url": "https://www.buymeacoffee.com/systeminfo"
}
},
"node_modules/tabbable": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
"integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
-1
View File
@@ -23,7 +23,6 @@
"react-bootstrap": "^2.10.10",
"react-bootstrap-icons": "^1.11.6",
"react-charts": "^3.0.0-beta.57",
"react-datepicker": "^9.1.0",
"react-dom": "^19.2.7",
"react-i18next": "^17.0.8",
"react-markdown": "^10.1.0",
@@ -28,6 +28,8 @@ describe('Portuguese Utils unit tests', () => {
expect(translateTimeMessage('1 month left', 'pt_br')).toBe('1 mês restante');
expect(translateTimeMessage('2 days left', 'pt_br')).toBe('2 dias restantes');
expect(translateTimeMessage('1 day left', 'pt_br')).toBe('1 dia restante');
expect(translateTimeMessage('Due tomorrow', 'pt_br')).toBe('Vence amanhã');
expect(translateTimeMessage('Due today', 'pt_br')).toBe('Vence hoje');
expect(translateTimeMessage('lala', 'pt_br')).toBe('lala');
expect(translateTimeMessage('null', 'pt_br')).toBe('null');
});
@@ -49,6 +49,8 @@ describe('Russian Utils unit tests', () => {
expect(translateTimeMessage('7 days left', 'ru')).toBe('осталось 7 дней');
expect(translateTimeMessage('8 days left', 'ru')).toBe('осталось 8 дней');
expect(translateTimeMessage('9 days left', 'ru')).toBe('осталось 9 дней');
expect(translateTimeMessage('Due tomorrow', 'ru')).toBe('Срок завтра');
expect(translateTimeMessage('Due today', 'ru')).toBe('Срок сегодня');
expect(translateTimeMessage('lala', 'ru')).toBe('lala');
expect(translateTimeMessage('null', 'ru')).toBe('null');
});
@@ -28,6 +28,8 @@ describe('Spanish Utils unit tests', () => {
expect(translateTimeMessage('1 month left', 'es')).toBe('Falta 1 mes');
expect(translateTimeMessage('2 days left', 'es')).toBe('Faltan 2 días');
expect(translateTimeMessage('1 day left', 'es')).toBe('Falta 1 día');
expect(translateTimeMessage('Due tomorrow', 'es')).toBe('Vence mañana');
expect(translateTimeMessage('Due today', 'es')).toBe('Vence hoy');
expect(translateTimeMessage('lala', 'es')).toBe('lala');
expect(translateTimeMessage('null', 'es')).toBe('null');
});
@@ -0,0 +1,6 @@
@import '../../styles/theme.scss';
.form-control[type="date"] {
font-size: 16px; /* Prevents iOS zoom on focus */
border-left: none;
}
@@ -1,47 +0,0 @@
@import '../../styles/theme.scss';
/* custom-datepicker.scss */
.react-datepicker-wrapper,
.react-datepicker__input-container {
flex: 1 1 auto !important;
width: 1% !important;
display: flex !important;
}
.react-datepicker__input-container {
/* Ensure it takes full width of the flex item */
width: 100%;
}
.react-datepicker__input-container input {
font-size: 16px; /* Prevents iOS zoom on focus */
width: 100%;
border-radius: 4px;
border: 1px solid var(--bs-border-color);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.react-datepicker-popper {
z-index: 9999 !important;
}
.react-datepicker__day {
margin: 0.2rem;
padding: 0.5rem;
border-radius: 4px;
}
/* Larger touch targets on mobile */
@media (max-width: 768px) {
.react-datepicker__day,
.react-datepicker__month-text,
.react-datepicker__quarter-text,
.react-datepicker__year-text {
width: 2.5rem;
height: 2.5rem;
line-height: 2.5rem;
margin: 0.2rem;
}
}
+10 -36
View File
@@ -1,11 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Col, Form, InputGroup, Row } from 'react-bootstrap';
import * as Icons from 'react-bootstrap-icons';
import DatePicker from 'react-datepicker';
import { MiddlewareReturn } from '@floating-ui/core';
import { MiddlewareState } from '@floating-ui/dom';
import 'react-datepicker/dist/react-datepicker.css';
import './custom-datepicker.scss';
import './FormInput.scss';
type IconName = keyof typeof Icons;
@@ -17,9 +13,7 @@ interface Props {
name: string;
placeholder?: string;
value?: string;
valueDate?: Date | null;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onChangeDate?: (date: Date | null) => void;
dataTestId?: string;
pwdHideText?: string;
pwdShowText?: string;
@@ -67,36 +61,16 @@ function FormInput(props: React.PropsWithChildren<Props>): React.ReactNode {
<InputGroup.Text>
<Icon />
</InputGroup.Text>
{props.type == 'date'
{props.type === 'date'
? (
<DatePicker
selected={props?.valueDate}
onChange={(date: Date | null) => {
if (props.onChangeDate) {
props.onChangeDate(date);
}
}}
dateFormat="MMMM d, yyyy"
className="form-control"
id="date-input"
placeholderText={props.placeholder}
popperPlacement="bottom"
popperModifiers={[
{
name: 'preventOverflow',
options: {
enabled: true,
boundariesElement: 'viewport'
},
fn: function (state: MiddlewareState): MiddlewareReturn | Promise<MiddlewareReturn> {
return state;
}
}
]}
withPortal
showYearDropdown
showMonthDropdown
dropdownMode="select"
<Form.Control
required={props.required}
type="date"
name={props.name}
placeholder={props.placeholder ? props.placeholder : ''}
value={props?.value}
onChange={props.onChange}
data-testid={props.dataTestId}
/>
)
: (
+7 -1
View File
@@ -19,6 +19,8 @@ export const timeAgoTranslations: Record<string, string> = {
'month left_pt_br': '{X} mês restante',
'days left_pt_br': '{X} dias restantes',
'day left_pt_br': '{X} dia restante',
'due tomorrow_pt_br': 'Vence amanhã',
'due today_pt_br': 'Vence hoje',
'years ago_es': 'Hace {X} años',
'year ago_es': 'Hace {X} año',
@@ -40,6 +42,8 @@ export const timeAgoTranslations: Record<string, string> = {
'month left_es': 'Falta {X} mes',
'days left_es': 'Faltan {X} días',
'day left_es': 'Falta {X} día',
'due tomorrow_es': 'Vence mañana',
'due today_es': 'Vence hoy',
'years ago_ru': '{X} года назад',
'year ago_ru': '{X} год назад',
@@ -60,7 +64,9 @@ export const timeAgoTranslations: Record<string, string> = {
'months left_ru': 'осталось {X} месяца',
'month left_ru': 'Остался {X} месяц',
'days left_ru': 'осталось {X} дня',
'day left_ru': 'Остался {X} день'
'day left_ru': 'Остался {X} день',
'due tomorrow_ru': 'Срок завтра',
'due today_ru': 'Срок сегодня'
};
export const serverResponsesTranslations: Record<string, string> = {
+1 -2
View File
@@ -277,8 +277,7 @@ code {
padding: 0.375rem 0.10rem 0.375rem 0.75rem;
}
.input-group > .form-control,
.react-datepicker__input-container > .form-control {
.input-group > .form-control {
border-left: none;
}
+8
View File
@@ -15,6 +15,14 @@ function translateTimeMessage(message: string, target: string): string {
return message;
}
if (message === 'Due tomorrow') {
return timeAgoTranslations[`due tomorrow_${target}`] ?? message;
}
if (message === 'Due today') {
return timeAgoTranslations[`due today_${target}`] ?? message;
}
const firstSpace = message.indexOf(' ');
const numberValue = message.substring(0, firstSpace);
const textValue = message.substring(firstSpace).trim();
+11 -15
View File
@@ -45,7 +45,7 @@ function TaskAdd(): React.ReactNode {
const [taskUrl, setTaskUrl] = useState<string>('');
const [taskDone, setTaskDone] = useState<boolean>(false);
const [action, setAction] = useState<TaskAction>('add');
const [dueDate, setDueDate] = useState<Date | null>(null);
const [dueDate, setDueDate] = useState<string>('');
const [highPriority, setHighPriority] = useState<boolean>(false);
const [currentTag, setCurrentTag] = useState<string>('');
const [selectedTags, setSelectedTags] = useState<string[]>([]);
@@ -127,7 +127,7 @@ function TaskAdd(): React.ReactNode {
setTaskDescription('');
setTaskDone(false);
setTaskUrl('');
setDueDate(null);
setDueDate('');
setHighPriority(false);
setCurrentTag('');
setSelectedTags([]);
@@ -138,7 +138,7 @@ function TaskAdd(): React.ReactNode {
const saveDraft = (
description: string,
taskUrl: string,
due: Date | null,
due: string,
priority: boolean,
draftTags: string[]
): void => {
@@ -148,7 +148,7 @@ function TaskAdd(): React.ReactNode {
const draft: TaskDraft = {
description,
taskUrl,
dueDate: due ? due.toISOString() : null,
dueDate: due || null,
highPriority: priority,
tags: draftTags
};
@@ -168,8 +168,8 @@ function TaskAdd(): React.ReactNode {
const draft: TaskDraft = JSON.parse(raw);
setTaskDescription(draft.description ?? '');
setTaskUrl(draft.taskUrl ?? '');
const parsedDate = draft.dueDate ? new Date(draft.dueDate) : null;
setDueDate(parsedDate && !isNaN(parsedDate.getTime()) ? parsedDate : null);
const parsedDate = draft.dueDate ? draft.dueDate : '';
setDueDate(parsedDate);
setHighPriority(draft.highPriority ?? false);
setSelectedTags(draft.tags ?? []);
setDraftBanner(true);
@@ -185,7 +185,7 @@ function TaskAdd(): React.ReactNode {
setTaskUrl(task.urls.length ? task.urls[0] : '');
setTaskDone(task.done);
if (task.dueDateFmt) {
setDueDate(new Date(task.dueDate));
setDueDate(task.dueDate);
}
setHighPriority(task.highPriority);
if (task.tags) {
@@ -249,10 +249,7 @@ function TaskAdd(): React.ReactNode {
return;
}
let dueDateFormatted: string = '';
if (dueDate) {
dueDateFormatted = dueDate.toISOString().substring(0, 10);
}
const dueDateFormatted: string = dueDate;
const finalTags = [...selectedTags];
if (currentTag.trim()) {
@@ -427,11 +424,10 @@ function TaskAdd(): React.ReactNode {
type="date"
name="dueDate"
placeholder={t('task_form_duedate_placeholder')}
valueDate={dueDate}
onChangeDate={(date: Date | null) => {
setDueDate(date);
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDueDate(e.target.value);
hasUserEdited.current = true;
saveDraft(taskDescription, taskUrl, date, highPriority, selectedTags);
saveDraft(taskDescription, taskUrl, e.target.value, highPriority, selectedTags);
}}
/>
</Col>
@@ -89,9 +89,9 @@ public class TimeAgoUtil {
} else if (period.getDays() > 1) {
sb.append(String.format("%d days left", period.getDays()));
} else if (period.getDays() > 0) {
sb.append(String.format("%d day left", period.getDays()));
sb.append("Due tomorrow");
} else if (period.getDays() == 0) {
sb.append("0 days left");
sb.append("Due today");
} else {
sb.append("Due");
}
@@ -41,7 +41,7 @@ class TimeAgoUtilTest {
Assertions.assertNull(TimeAgoUtil.formatDueDate(null));
LocalDate localDate1 = LocalDate.now().plusDays(1L);
String expected1 = "1 day left" + getFormattedSuffix(localDate1);
String expected1 = "Due tomorrow" + getFormattedSuffix(localDate1);
Assertions.assertEquals(expected1, TimeAgoUtil.formatDueDate(localDate1));
LocalDate localDate2 = LocalDate.now().plusDays(12L);
@@ -74,7 +74,7 @@ class TimeAgoUtilTest {
void formatDueDateEdgeCasesTest() {
// Test for today
LocalDate today = LocalDate.now();
String expectedToday = "0 days left" + getFormattedSuffix(today);
String expectedToday = "Due today" + getFormattedSuffix(today);
Assertions.assertEquals(expectedToday, TimeAgoUtil.formatDueDate(today));
// Test for a past date