Skip to content
Open
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"lint": "eslint --fix .",
"checklint": "eslint .",
"check:contributing-versions": "node .github/scripts/check-contributing-versions.cjs",
"test:unit": "node --test src/utils/build-collections.test.js src/utils/page-path-guard.test.js",
"test:unit": "node --test src/utils/build-collections.test.js src/utils/page-path-guard.test.js src/utils/resource-sort.test.js",
"pretest": "eslint --ignore-path .gitignore .",
"deploy": "gatsby build && gh-pages -d public -b master",
"noIndex": "gatsby build && echo 'User-agent: *\nDisallow: /' > public/robots.txt",
Expand Down
2 changes: 2 additions & 0 deletions src/sections/Resources/Resources-grid/DataWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const DataWrapper = (WrappedComponent) => {
id
frontmatter {
title
date
type
technology
product
Expand All @@ -37,6 +38,7 @@ const DataWrapper = (WrappedComponent) => {
}
fields {
slug
dateForSort
}
}
}
Expand Down
44 changes: 36 additions & 8 deletions src/sections/Resources/Resources-grid/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import React from "react";
import React, { useState } from "react";
import Card from "../../../components/Card";
import { Row, Col } from "../../../reusecore/Layout";
import Pagination from "./paginate";
import SearchBox from "../../../reusecore/Search";
import EmptyResources from "../Resources-error/emptyStateTemplate";
import { sortResources, paginateResources } from "../../../utils/resource-sort";

import { ResourcePageWrapper } from "./resourceGrid.style";

const ResourceGrid = (props) => {
const query = props.searchQuery || "";
const hasQuery = Boolean(query);
const [choice, setChoice] = useState(null);
const effectiveSort =
choice && choice.query === query
? choice.value
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
: hasQuery
? "relevance"
: "latest";

// Get current posts
const indexOfLastPost = props.currentPage * props.postsPerPage;
const indexOfFirstPost = indexOfLastPost - props.postsPerPage;
const searchedResource =
props.postsPerPage > 0
? props.data.slice(indexOfFirstPost, indexOfLastPost)
: props.data;
const sortedData = sortResources(props.data, effectiveSort);
const searchedResource = paginateResources(
sortedData,
props.currentPage,
props.postsPerPage
);

const paginate = (pageNumber) => {
props.setCurrentPage(pageNumber);
Expand All @@ -29,6 +40,23 @@ const ResourceGrid = (props) => {
<ResourcePageWrapper>
<div className="resource-grid-wrapper">
<div className="search">
<div className="sortBox">
<select
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
className="sortDropdown"
aria-label="Sort by"
value={effectiveSort}
onChange={(e) => {
setChoice({ value: e.target.value, query });
props.setCurrentPage(1);
}}
>
<option value="latest">Latest</option>
<option value="oldest">Oldest</option>
<option value="relevance" disabled={!hasQuery}>
Relevance
</option>
</select>
</div>
<div className="searchBox">
<SearchBox
searchQuery={props.searchQuery}
Expand Down Expand Up @@ -79,4 +107,4 @@ const ResourceGrid = (props) => {
);
};

export default ResourceGrid;
export default ResourceGrid;
65 changes: 60 additions & 5 deletions src/sections/Resources/Resources-grid/resourceGrid.style.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,71 @@ export const ResourcePageWrapper = styled.div`
.search{
display:flex;
justify-content:flex-end;
align-items:center;
width:100%;

margin-bottom: 0.8rem;
.searchBox{
flex:0 0 50%;
flex: 0 1 400px;
min-width:0;
& > div,
.search-box{
width: 100%;
max-width: 100%;
}
@media only screen and (max-width:990px){

flex:0 0 100%;
max-width:100%;

flex: 1 1 auto;
min-width: 0;
.search-box{
width: 100%;
}
}
}
.sortBox {
align-self: stretch;
margin-right: 1rem;
@media only screen and (max-width: 600px) {
order: 2;
margin-right: 0;
margin-left: 1rem;
}
.sortDropdown {
height: 100%;
padding: 15px 40px 15px 15px;
border-radius: 5px;
border: 1px solid ${(props) => props.theme.headingColor};
background-color: ${(props) => props.theme.shadowDarkColor};
color: ${(props) => props.theme.text};
font-size: 0.9rem;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;

&:focus {
outline: 2px solid #00b39f;
outline-offset: 2px;
}

option {
color: ${(props) => props.theme.text};
background-color: ${(props) => props.theme.shadowDarkColor};
}

@media only screen and (max-width: 600px) {
width: 48px;
height:48px;
padding: 0;
font-size:0;
color: transparent;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round'%3e%3cline x1='4' y1='7' x2='20' y2='7'/%3e%3cline x1='4' y1='12' x2='15' y2='12'/%3e%3cline x1='4' y1='17' x2='10' y2='17'/%3e%3c/svg%3e");
background-position: center;
background-size: 22px;
}
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/utils/resource-sort.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const getTime = (node) => {
const t = new Date(node.fields?.dateForSort).getTime();
return Number.isNaN(t) || t <= 0 ? null : t;
};

const sortResources = (nodes, sortOption) => {
if (sortOption === "relevance") return nodes;

const direction = sortOption === "oldest" ? 1 : -1;
return nodes.slice().sort((a, b) => {
const ta = getTime(a);
const tb = getTime(b);
if (ta === null && tb === null) return 0;
if (ta === null) return 1;
if (tb === null) return -1;
return direction * (ta - tb);
});
};

const paginateResources = (items, currentPage, postsPerPage) => {
if (!(postsPerPage > 0)) return items;
const last = currentPage * postsPerPage;
return items.slice(last - postsPerPage, last);
};

module.exports = { getTime, sortResources, paginateResources };
52 changes: 52 additions & 0 deletions src/utils/resource-sort.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const { sortResources, paginateResources } = require("./resource-sort");

const node = (id, dateForSort) => ({ id, fields: { dateForSort } });

const mixed = [
node("b", "2024-02-01T00:00:00.000Z"),
node("missing", undefined),
node("c", "2025-03-01T00:00:00.000Z"),
node("invalid", "not-a-date"),
node("a", "2023-01-01T00:00:00.000Z"),
];

const ids = (list) => list.map((n) => n.id);

test("latest sorts newest first", () => {
assert.deepEqual(ids(sortResources(mixed, "latest")).slice(0, 3), ["c", "b", "a"]);
});

test("oldest sorts oldest first", () => {
assert.deepEqual(ids(sortResources(mixed, "oldest")).slice(0, 3), ["a", "b", "c"]);
});

test("missing or invalid dates are always last", () => {
for (const option of ["latest", "oldest"]) {
const tail = ids(sortResources(mixed, option)).slice(3);
assert.deepEqual(tail.sort(), ["invalid", "missing"]);
}
});

test("relevance keeps the incoming order", () => {
assert.deepEqual(ids(sortResources(mixed, "relevance")), ids(mixed));
});

test("sorting does not mutate the input", () => {
const before = ids(mixed);
sortResources(mixed, "latest");
assert.deepEqual(ids(mixed), before);
});

const items = [1, 2, 3, 4, 5, 6, 7];

test("pagination returns the right slice for each page", () => {
assert.deepEqual(paginateResources(items, 1, 3), [1, 2, 3]);
assert.deepEqual(paginateResources(items, 2, 3), [4, 5, 6]);
assert.deepEqual(paginateResources(items, 3, 3), [7]);
});

test("pagination returns everything when postsPerPage is 0", () => {
assert.deepEqual(paginateResources(items, 1, 0), items);
});
Loading