diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e8055a65..ccf6c592 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "@vitejs/plugin-react-swc": "^3.3.2", "aws-amplify": "^6.7.0", "axios": "^1.6.8", + "date-fns": "^4.1.0", "jspdf": "^2.5.2", "jspdf-autotable": "^3.8.3", "leaflet": "^1.9.4", @@ -1725,6 +1726,16 @@ "react-dom": "^16.8.6 || ^17.0.1 || ^18.2.0" } }, + "node_modules/@carbon/charts/node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/@carbon/colors": { "version": "11.28.0", "resolved": "https://registry.npmjs.org/@carbon/colors/-/colors-11.28.0.tgz", @@ -7170,9 +7181,9 @@ } }, "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", "funding": { "type": "github", diff --git a/frontend/package.json b/frontend/package.json index 4646a57d..f1023b8c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "@vitejs/plugin-react-swc": "^3.3.2", "aws-amplify": "^6.7.0", "axios": "^1.6.8", + "date-fns": "^4.1.0", "jspdf": "^2.5.2", "jspdf-autotable": "^3.8.3", "leaflet": "^1.9.4", diff --git a/frontend/src/__test__/components/FriendlyDate.test.tsx b/frontend/src/__test__/components/FriendlyDate.test.tsx new file mode 100644 index 00000000..bb3db789 --- /dev/null +++ b/frontend/src/__test__/components/FriendlyDate.test.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { vi } from 'vitest'; +import FriendlyDate from '../../components/FriendlyDate'; + +// Mock Tooltip component from Carbon to ensure tests run without extra dependencies +vi.mock('@carbon/react', () => { + const Tooltip = ({ label, children }) => {children}; + return { Tooltip }; +}); + +// Mock `Date.now` for consistent testing +beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2024-01-24T00:00:00')); // Set a fixed date for testing +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe('FriendlyDate Component', () => { + + it('displays "Today" for today\'s date', () => { + render(); + expect(screen.getByText("Today")).toBeInTheDocument(); + }); + + it('displays "Yesterday" for a date one day ago', () => { + render(); + expect(screen.getByText("Yesterday")).toBeInTheDocument(); + }); + + it('displays relative time within the last week', () => { + render(); + expect(screen.getByText("3 days ago")).toBeInTheDocument(); + }); + + it('displays exact date for dates older than a week', () => { + render(); + expect(screen.getByText("23 days ago")).toBeInTheDocument(); + }); + + it('displays friendly date format for future dates', () => { + render(); + expect(screen.getByText("in 29 days")).toBeInTheDocument(); + }); + + it('renders tooltip with full text on hover', async () => { + const {container} = render(); + expect(container.querySelector('span').getAttribute('data-tooltip')).toBe("Feb 22, 2024"); + }); + + it('renders an empty span for null dates', () => { + const {getByTestId} = render(); + expect(getByTestId("friendly-date")).toBeInTheDocument(); + + }); + + it('renders an empty span for undefined dates', () => { + const {getByTestId} = render(); + expect(getByTestId("friendly-date")).toBeInTheDocument(); + + }); + + it('renders an empty span for invalid', () => { + const {getByTestId} = render(); + expect(getByTestId("friendly-date")).toBeInTheDocument(); + + }); + + it('renders an empty span for empty', () => { + const {getByTestId} = render(); + expect(getByTestId("friendly-date")).toBeInTheDocument(); + + }); + + +}); diff --git a/frontend/src/components/FriendlyDate/index.tsx b/frontend/src/components/FriendlyDate/index.tsx new file mode 100644 index 00000000..e49acbd2 --- /dev/null +++ b/frontend/src/components/FriendlyDate/index.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { formatDistanceToNow, format, parseISO, isToday, isYesterday } from 'date-fns'; +import { Tooltip } from '@carbon/react'; + +interface FriendlyDateProps { + date: string | null | undefined; // The date string in ISO format +} + +const FriendlyDate: React.FC = ({ date }) => { + + if(!date) return ; + + try{ + const parsedDate = parseISO(date); + const cleanDate = format(parsedDate, "MMM dd, yyyy"); + + const formattedDate = isToday(parsedDate) + ? "Today" + : isYesterday(parsedDate) + ? "Yesterday" + : formatDistanceToNow(parsedDate, { addSuffix: true }); + + return + {formattedDate} + ; + } catch(e){ + return ; + } +}; + +export default FriendlyDate; \ No newline at end of file diff --git a/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchBar/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchBar/index.tsx index 173840aa..ad4fb002 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchBar/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchBar/index.tsx @@ -31,6 +31,7 @@ const OpeningsSearchBar: React.FC = ({ const handleSearchClick = () => { onSearchClick(); + setIsOpen(false); }; const handleInputChange = (e: React.ChangeEvent) => { diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx index f01053dc..1b62d519 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx @@ -43,6 +43,8 @@ import { useNavigate } from "react-router-dom"; import { setOpeningFavorite } from '../../../../services/OpeningFavouriteService'; import { useNotification } from "../../../../contexts/NotificationProvider"; import TruncatedText from "../../../TruncatedText"; +import FriendlyDate from "../../../FriendlyDate"; + interface ISearchScreenDataTable { rows: OpeningsSearch[]; @@ -374,7 +376,9 @@ const SearchScreenDataTable: React.FC = ({ - ) : ( + ) : header.key === 'disturbanceStartDate' ? ( + + ) : ( row[header.key] )} @@ -401,7 +405,7 @@ const SearchScreenDataTable: React.FC = ({ backwardText="Previous page" forwardText="Next page" pageSize={itemsPerPage} - pageSizes={[5, 20, 50, 200, 400]} + pageSizes={[20, 40, 60, 80, 100]} itemsPerPageText="Items per page" page={currentPage} onChange={({ diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts index 92ce6801..cd59d624 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts @@ -50,7 +50,7 @@ export const columns: ITableHeader[] = [ { key: 'disturbanceStartDate', header: 'Disturbance Date', - selected: false + selected: true } ]; diff --git a/frontend/src/contexts/PaginationProvider.tsx b/frontend/src/contexts/PaginationProvider.tsx index 4a89251a..5ae52963 100644 --- a/frontend/src/contexts/PaginationProvider.tsx +++ b/frontend/src/contexts/PaginationProvider.tsx @@ -3,9 +3,9 @@ import PaginationContext, { PaginationContextData } from "./PaginationContext"; const PaginationProvider: React.FC<{children: React.ReactNode}> = ({ children }) => { const [data, setData] = useState([]); - const [initialItemsPerPage, setInitialItemsPerPage] = useState(0); + const [initialItemsPerPage, setInitialItemsPerPage] = useState(20); const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage, setItemsPerPage] = useState(5); + const [itemsPerPage, setItemsPerPage] = useState(20); const [totalResultItems, setTotalResultItems] = useState(0); // State for totalResultItems // Update the total number of pages when itemsPerPage changes