Group three changes in a single PR (#13)
Main CI-Frontend / Build & Push (push) Successful in 46s
Main CI-Backend / Build & Push (push) Successful in 54s

## What

- fix: background image position moving around in the landing, login and register pages.
- chore: bump minor dependencies versions to latest version.
- feat: replace "Due in 1 day" and "Due in 0 days" to Time Ago format: "Due tomorrow" and "Due today".

## Why

- Keeping the app improving and updated.
- Date picker in mobile works better using the default/native.

## Mood
<img width="200" src="https://media0.giphy.com/media/TIiDh0ONXotAQpkw3f/giphy.gif?cid=5a38a5a2cea0tfjgne8sdpz1vz617kfqk02hnjrrjx34v08x&amp;ep=v1_gifs_search&amp;rid=giphy.gif&amp;ct=g" style="border:0;height:300px;width:300px"/>

Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
2026-07-20 16:40:53 +00:00
parent 891c526363
commit 0250b558cf
17 changed files with 367 additions and 557 deletions
+12
View File
@@ -7,6 +7,18 @@ 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.
- Bumped all minor deps in the frontend.
### Fixed
- Background image position in landing, login and register pages.
### Removed
- React Date Picker dependency in favour of regular browser input date UI.
## app-v2026.07.01.140 - 2026-07-01
### Added
+281 -432
View File
File diff suppressed because it is too large Load Diff
+17 -18
View File
@@ -13,25 +13,24 @@
"private": true,
"dependencies": {
"@popperjs/core": "^2.11.8",
"@types/node": "^26.0.1",
"@types/node": "^26.1.1",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"bootstrap": "^5.3.8",
"dompurify": "^3.4.11",
"i18next": "^26.3.2",
"dompurify": "^3.4.12",
"i18next": "^26.3.6",
"react": "^19.2.7",
"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-i18next": "^17.0.10",
"react-markdown": "^10.1.0",
"react-router": "^8.0.1",
"react-router": "^8.2.0",
"react-router-bootstrap": "^0.26.3",
"remark-gfm": "^4.0.1",
"typescript": "^6.0.3",
"vite": "^8.1.0"
"vite": "^8.1.5"
},
"scripts": {
"start": "vite --host",
@@ -66,8 +65,8 @@
},
"devDependencies": {
"@eslint/compat": "^2.1.0",
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^9.39.4",
"@eslint/eslintrc": "^3.3.6",
"@eslint/js": "^9.39.5",
"@stylistic/eslint-plugin": "^5.10.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
@@ -75,21 +74,21 @@
"@types/jest": "^30.0.0",
"@types/react": "^19.2.17",
"@types/react-router-bootstrap": "^0.26.8",
"@vitest/coverage-v8": "^4.1.9",
"cypress": "^15.18.0",
"eslint": "^9.39.4",
"@vitest/coverage-v8": "^4.1.10",
"cypress": "^15.18.1",
"eslint": "^9.39.5",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import-x": "^4.17.0",
"eslint-plugin-jsdoc": "^63.0.7",
"eslint-plugin-n": "^18.1.0",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-jsdoc": "^63.2.0",
"eslint-plugin-n": "^18.2.2",
"eslint-plugin-promise": "^7.3.0",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.7.0",
"jsdom": "^29.1.1",
"prettier": "^3.8.4",
"prettier": "^3.9.5",
"sass": "^1.101.0",
"source-map-support": "^0.5.21",
"typescript-eslint": "^8.62.0",
"vitest": "^4.1.9"
"typescript-eslint": "^8.64.0",
"vitest": "^4.1.10"
}
}
@@ -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();
+2 -1
View File
@@ -4,8 +4,9 @@
position: relative;
text-align: center;
color: $dark-text;
background: var(--bs-landing-bg) no-repeat center center;
background: var(--bs-landing-bg) no-repeat center center fixed;
background-size: cover; // Ensures the image covers the whole background
min-height: 100vh;
&::before {
content: '';
+2 -1
View File
@@ -1,11 +1,12 @@
@import '../../styles/theme.scss';
.login-page {
position: relative;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: var(--bs-landing-bg) no-repeat center center;
background: var(--bs-landing-bg) no-repeat center center fixed;
background-size: cover;
&::before {
+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