diff --git a/src/components/extension-card.js b/src/components/extension-card.js
index f5a2f9040cd2..ee1d71bc02b5 100644
--- a/src/components/extension-card.js
+++ b/src/components/extension-card.js
@@ -9,12 +9,11 @@ import ExtensionImage from "./extension-image"
const Card = styled(props => )`
font-size: 3.5em;
text-align: center;
- margin: 15px;
padding: 1rem;
width: 100%;
background: var(--white) 0 0 no-repeat padding-box;
border: ${props =>
- props.$unlisted ? "1px solid var(--grey-0)" : "1px solid var(--grey-1)"};
+ props.$unlisted ? "1px solid var(--grey-0)" : "1px solid var(--grey-1)"};
border-radius: 10px;
opacity: 1;
display: flex;
@@ -141,9 +140,9 @@ const ExtensionCard = ({ extension }) => {
{extension.metadata.maven?.timestamp &&
isValid(+extension.metadata.maven?.timestamp)
? `Publish Date: ${format(
- new Date(+extension.metadata.maven.timestamp),
- "MMM dd, yyyy"
- )}`
+ new Date(+extension.metadata.maven.timestamp),
+ "MMM dd, yyyy"
+ )}`
: spacer}
diff --git a/src/components/extensions-list.js b/src/components/extensions-list.js
index efda809853e5..0bae74687653 100644
--- a/src/components/extensions-list.js
+++ b/src/components/extensions-list.js
@@ -3,12 +3,12 @@ import { useState } from "react"
import Filters from "./filters/filters"
import ExtensionCard from "./extension-card"
import styled from "styled-components"
-import { extensionComparator } from "./util/extension-comparator"
+import { timestampExtensionComparator } from "./sortings/timestamp-extension-comparator"
+import Sortings from "./sortings/sortings"
const FilterableList = styled.div`
margin-left: var(--site-margins);
margin-right: var(--site-margins);
- margin-top: 85px;
display: flex;
flex-direction: row;
justify-content: space-between;
@@ -17,9 +17,9 @@ const FilterableList = styled.div`
const Extensions = styled.ol`
list-style: none;
display: grid;
+ gap: 30px;
grid-template-columns: repeat(auto-fill, minmax(260px, auto));
grid-template-rows: repeat(auto-fill, 1fr);
- width: 100%;
`
const CardItem = styled.li`
@@ -28,6 +28,14 @@ const CardItem = styled.li`
display: flex;
`
+const InfoSortRow = styled.div`
+ margin-top: 85px;
+ padding-left: var(--site-margins);
+ padding-right: var(--site-margins);
+ display: flex;
+ justify-content: space-between;
+`
+
const RightColumn = styled.div`
display: flex;
flex-direction: column;
@@ -37,11 +45,11 @@ const RightColumn = styled.div`
const ExtensionCount = styled.h2`
margin-top: 1.25rem;
- margin-left: 3.25rem;
margin-bottom: 0.5rem;
width: 100%;
- font-size: 1.25rem;
+ font-size: 1rem;
font-weight: 400;
+ font-style: italic;
`
const ExtensionsList = ({ extensions, categories }) => {
@@ -49,8 +57,8 @@ const ExtensionsList = ({ extensions, categories }) => {
const allExtensions = extensions.filter(extension => !extension.isSuperseded)
const [filteredExtensions, setExtensions] = useState(allExtensions)
+ const [extensionComparator, setExtensionComparator] = useState(() => timestampExtensionComparator)
- // TODO why is this guard necessary?
if (allExtensions) {
// Exclude unlisted extensions from the count, even though we sometimes show them if there's a direct search for it
const extensionCount = allExtensions.filter(
@@ -65,26 +73,30 @@ const ExtensionsList = ({ extensions, categories }) => {
: `Showing ${filteredExtensions.length} matching of ${extensionCount} extensions`
return (
-
-
-
- {" "}
- {countMessage}
-
- {filteredExtensions.map(extension => {
- return (
-
-
-
- )
- })}
- {" "}
-
-
+
+ {countMessage}
+
+
+
+
+
+ {" "}
+
+ {filteredExtensions.map(extension => {
+ return (
+
+
+
+ )
+ })}
+ {" "}
+
+
+
)
} else {
return (
diff --git a/src/components/sortings/alphabetical-extension-comparator.js b/src/components/sortings/alphabetical-extension-comparator.js
new file mode 100644
index 000000000000..912355cf11f8
--- /dev/null
+++ b/src/components/sortings/alphabetical-extension-comparator.js
@@ -0,0 +1,46 @@
+const HOURS_IN_MS = 60 * 60 * 1000
+
+function compareByTimestamp(a, b) {
+ const timestampA = roundToTheNearestHour(a?.metadata?.maven?.timestamp)
+ const timestampB = roundToTheNearestHour(b?.metadata?.maven?.timestamp)
+
+ if (timestampA && timestampB) {
+ const delta = timestampB - timestampA
+ if (delta === 0) {
+ return compareAlphabetically(a, b)
+ } else {
+ return delta
+ }
+ } else if (timestampA) {
+ return -1
+ } else if (timestampB) {
+ return 1
+ }
+}
+
+const alphabeticalExtensionComparator = (a, b) => {
+
+ let comp = compareAlphabetically(a, b)
+ if (comp === 0) {
+ comp = compareByTimestamp(a, b)
+ }
+ return comp
+}
+
+function roundToTheNearestHour(n) {
+ if (n) {
+ return Math.round(n / HOURS_IN_MS) * HOURS_IN_MS
+ }
+}
+
+function compareAlphabetically(a, b) {
+ if (a.sortableName) {
+ return a.sortableName.localeCompare(b.sortableName)
+ } else if (b.sortableName) {
+ return 1
+ } else {
+ return 0
+ }
+}
+
+module.exports = { alphabeticalExtensionComparator }
\ No newline at end of file
diff --git a/src/components/sortings/alphabetical-extension-comparator.test.js b/src/components/sortings/alphabetical-extension-comparator.test.js
new file mode 100644
index 000000000000..9ff676cb2ee0
--- /dev/null
+++ b/src/components/sortings/alphabetical-extension-comparator.test.js
@@ -0,0 +1,28 @@
+import { alphabeticalExtensionComparator } from "./alphabetical-extension-comparator"
+
+describe("the alphabetical extension comparator", () => {
+ it("sorts by date when the names are identical", () => {
+ const a = { sortableName: "alpha", metadata: { maven: { timestamp: 1795044005 } } }
+ const b = { sortableName: "alpha", metadata: { maven: { timestamp: 1695044005 } } }
+
+ expect(alphabeticalExtensionComparator(a, b)).toBeLessThan(0)
+ expect(alphabeticalExtensionComparator(b, a)).toBeGreaterThan(0)
+ })
+
+ it("put extensions with a name ahead of those without", () => {
+ const a = { sortableName: "alpha" }
+ const b = {}
+
+ expect(alphabeticalExtensionComparator(a, b)).toBe(-1)
+ expect(alphabeticalExtensionComparator(b, a)).toBe(1)
+ })
+
+ it("sorts alphabetically", () => {
+ const a = { sortableName: "alpha", metadata: { maven: { timestamp: 1795044005 } } }
+ const b = { sortableName: "beta", metadata: { maven: { timestamp: 1695044005 } } }
+
+ expect(alphabeticalExtensionComparator(a, b)).toBeLessThan(0)
+ expect(alphabeticalExtensionComparator(b, a)).toBeGreaterThan(0)
+ })
+
+})
\ No newline at end of file
diff --git a/src/components/sortings/sortings.js b/src/components/sortings/sortings.js
new file mode 100644
index 000000000000..7adee31cc55e
--- /dev/null
+++ b/src/components/sortings/sortings.js
@@ -0,0 +1,90 @@
+import * as React from "react"
+import styled from "styled-components"
+import Select from "react-select"
+import { styles } from "../util/styles/style"
+import { timestampExtensionComparator } from "./timestamp-extension-comparator"
+import { alphabeticalExtensionComparator } from "./alphabetical-extension-comparator"
+
+const Title = styled.label`
+ font-size: var(--font-size-16);
+ letter-spacing: 0;
+ color: var(--grey-2);
+ width: 100px;
+ text-align: right;
+`
+
+const SortBar = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ gap: var(--a-small-space);
+`
+
+const Element = styled.form`
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ gap: 16px;
+`
+
+// Grab CSS variables in javascript
+const grey = styles["grey-2"]
+
+const colourStyles = {
+ control: styles => ({
+ ...styles,
+ borderRadius: 0,
+ color: grey,
+ borderColor: grey,
+ width: "280px",
+ }),
+ option: (styles, { isDisabled }) => {
+ return {
+ ...styles,
+ cursor: isDisabled ? "not-allowed" : "default",
+ borderRadius: 0,
+ }
+ },
+ dropdownIndicator: styles => ({
+ ...styles,
+ color: grey, // Custom colour
+ }),
+ indicatorSeparator: styles => ({
+ ...styles,
+ margin: 0,
+ backgroundColor: grey,
+ }),
+}
+
+const sortings = [
+ { label: "most recently released", value: "time", comparator: timestampExtensionComparator },
+ { label: "alphabetical", value: "alpha", comparator: alphabeticalExtensionComparator }]
+
+const Sortings = ({ sorterAction }) => {
+
+ const setSortByDescription = (entry) => {
+ // We need to wrap our comparator functions in functions or they get called, which goes very badly
+ sorterAction && sorterAction(() => entry.comparator)
+ }
+
+ return (
+
+ Sort by
+
+
+
+ )
+}
+
+export default Sortings
+
diff --git a/src/components/sortings/sortings.test.js b/src/components/sortings/sortings.test.js
new file mode 100644
index 000000000000..0b58cc1c1836
--- /dev/null
+++ b/src/components/sortings/sortings.test.js
@@ -0,0 +1,76 @@
+import React from "react"
+import { render, screen } from "@testing-library/react"
+import userEvent from "@testing-library/user-event"
+import Sortings from "./sortings"
+import selectEvent from "react-select-event"
+import { alphabeticalExtensionComparator } from "./alphabetical-extension-comparator"
+import { timestampExtensionComparator } from "./timestamp-extension-comparator"
+
+
+let mockQueryParamSearchString = undefined
+
+jest.mock("react-use-query-param-string", () => {
+
+ const original = jest.requireActual("react-use-query-param-string")
+ return {
+ ...original,
+ useQueryParamString: jest.fn().mockImplementation(() => [mockQueryParamSearchString, jest.fn().mockImplementation((val) => mockQueryParamSearchString = val), true]),
+ getQueryParams: jest.fn().mockReturnValue({ "search-regex": mockQueryParamSearchString })
+
+ }
+})
+
+describe("sorting bar", () => {
+ const sortListener = jest.fn()
+
+ beforeEach(() => {
+ mockQueryParamSearchString = undefined
+ render(
+
+ )
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+
+ describe("sorting", () => {
+ userEvent.setup()
+ const label = "Sort by"
+
+ it("lets the listener know when a new sort scheme is chosen", async () => {
+ expect(screen.getByTestId("sort-form")).toHaveFormValues({
+ "sort": "",
+ })
+ await selectEvent.select(screen.getByLabelText(label), "most recently released")
+
+ expect(sortListener).toHaveBeenCalled()
+ })
+
+ it("lets the listener know when a new timestamp sort scheme is chosen", async () => {
+ expect(screen.getByTestId("sort-form")).toHaveFormValues({
+ "sort": "",
+ })
+ await selectEvent.select(screen.getByLabelText(label), "most recently released")
+
+ expect(sortListener).toHaveBeenCalledWith(expect.any(Function))
+ const param = sortListener.mock.calls[0][0]
+ expect(param()).toEqual(timestampExtensionComparator)
+ })
+
+ it("lets the listener know when an alphabetical scheme is chosen", async () => {
+ expect(screen.getByTestId("sort-form")).toHaveFormValues({
+ "sort": "",
+ })
+ await selectEvent.select(screen.getByLabelText(label), "alphabetical")
+
+ expect(sortListener).toHaveBeenCalledWith(expect.any(Function))
+ const param = sortListener.mock.calls[0][0]
+ expect(param()).toEqual(alphabeticalExtensionComparator)
+ })
+
+ })
+})
diff --git a/src/components/util/extension-comparator.js b/src/components/sortings/timestamp-extension-comparator.js
similarity index 88%
rename from src/components/util/extension-comparator.js
rename to src/components/sortings/timestamp-extension-comparator.js
index 496c88ad29fa..917e3083eaa7 100644
--- a/src/components/util/extension-comparator.js
+++ b/src/components/sortings/timestamp-extension-comparator.js
@@ -1,7 +1,7 @@
const HOURS_IN_MS = 60 * 60 * 1000
-const extensionComparator = (a, b) => {
-
+const timestampExtensionComparator = (a, b) => {
+
const timestampA = roundToTheNearestHour(a?.metadata?.maven?.timestamp)
const timestampB = roundToTheNearestHour(b?.metadata?.maven?.timestamp)
@@ -36,4 +36,4 @@ function compareAlphabetically(a, b) {
}
}
-module.exports = { extensionComparator }
\ No newline at end of file
+module.exports = { timestampExtensionComparator }
\ No newline at end of file
diff --git a/src/components/util/extension-comparator.test.js b/src/components/sortings/timestamp-extension-comparator.test.js
similarity index 61%
rename from src/components/util/extension-comparator.test.js
rename to src/components/sortings/timestamp-extension-comparator.test.js
index ef297de93bee..7b7c7f8797a1 100644
--- a/src/components/util/extension-comparator.test.js
+++ b/src/components/sortings/timestamp-extension-comparator.test.js
@@ -1,50 +1,50 @@
-import { extensionComparator } from "./extension-comparator"
+import { timestampExtensionComparator } from "./timestamp-extension-comparator"
-describe("the extension comparator", () => {
+describe("the timestamp extension comparator", () => {
it("sorts alphabetically when there is no date", () => {
const a = { sortableName: "alpha" }
const b = { sortableName: "beta" }
- expect(extensionComparator(a, b)).toBe(-1)
- expect(extensionComparator(b, a)).toBe(1)
+ expect(timestampExtensionComparator(a, b)).toBe(-1)
+ expect(timestampExtensionComparator(b, a)).toBe(1)
})
it("put extensions with a name ahead of those without", () => {
const a = { sortableName: "alpha" }
const b = {}
- expect(extensionComparator(a, b)).toBe(-1)
- expect(extensionComparator(b, a)).toBe(1)
+ expect(timestampExtensionComparator(a, b)).toBe(-1)
+ expect(timestampExtensionComparator(b, a)).toBe(1)
})
it("sorts by date", () => {
const a = { metadata: { maven: { timestamp: 1695044005 } } }
const b = { metadata: { maven: { timestamp: 1095044005 } } }
- expect(extensionComparator(a, b)).toBeLessThan(0)
- expect(extensionComparator(b, a)).toBeGreaterThan(0)
+ expect(timestampExtensionComparator(a, b)).toBeLessThan(0)
+ expect(timestampExtensionComparator(b, a)).toBeGreaterThan(0)
})
it("puts extensions with a date ahead of those without", () => {
const a = { metadata: { maven: { timestamp: 1695044005 } } }
const b = {}
- expect(extensionComparator(a, b)).toBe(-1)
- expect(extensionComparator(b, a)).toBe(1)
+ expect(timestampExtensionComparator(a, b)).toBe(-1)
+ expect(timestampExtensionComparator(b, a)).toBe(1)
})
it("returns 0 when the dates are equal and there is no name", () => {
const a = { metadata: { maven: { timestamp: 1695044005 } } }
- expect(extensionComparator(a, a)).toBe(0)
+ expect(timestampExtensionComparator(a, a)).toBe(0)
})
it("sorts alphabetically when the dates are equal", () => {
const a = { sortableName: "alpha", metadata: { maven: { timestamp: 1695044005 } } }
const b = { sortableName: "beta", metadata: { maven: { timestamp: 1695044005 } } }
- expect(extensionComparator(a, b)).toBe(-1)
- expect(extensionComparator(b, a)).toBe(1)
+ expect(timestampExtensionComparator(a, b)).toBe(-1)
+ expect(timestampExtensionComparator(b, a)).toBe(1)
})
// If extensions are released at roughly the same time, their timestamp will be different, but we should group them alphabetically
@@ -52,8 +52,8 @@ describe("the extension comparator", () => {
const a = { sortableName: "alpha", metadata: { maven: { timestamp: 1695044005 } } }
const b = { sortableName: "beta", metadata: { maven: { timestamp: 1695040465 } } }
- expect(extensionComparator(a, b)).toBe(-1)
- expect(extensionComparator(b, a)).toBe(1)
+ expect(timestampExtensionComparator(a, b)).toBe(-1)
+ expect(timestampExtensionComparator(b, a)).toBe(1)
})
})
\ No newline at end of file