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