Skip to content

Commit aef5849

Browse files
committed
Allow sorting alphabetically and chronologically
1 parent b8a3069 commit aef5849

8 files changed

+300
-49
lines changed

src/components/extension-card.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ import ExtensionImage from "./extension-image"
99
const Card = styled(props => <Link {...props} />)`
1010
font-size: 3.5em;
1111
text-align: center;
12-
margin: 15px;
1312
padding: 1rem;
1413
width: 100%;
1514
background: var(--white) 0 0 no-repeat padding-box;
1615
border: ${props =>
17-
props.$unlisted ? "1px solid var(--grey-0)" : "1px solid var(--grey-1)"};
16+
props.$unlisted ? "1px solid var(--grey-0)" : "1px solid var(--grey-1)"};
1817
border-radius: 10px;
1918
opacity: 1;
2019
display: flex;
@@ -141,9 +140,9 @@ const ExtensionCard = ({ extension }) => {
141140
{extension.metadata.maven?.timestamp &&
142141
isValid(+extension.metadata.maven?.timestamp)
143142
? `Publish Date: ${format(
144-
new Date(+extension.metadata.maven.timestamp),
145-
"MMM dd, yyyy"
146-
)}`
143+
new Date(+extension.metadata.maven.timestamp),
144+
"MMM dd, yyyy"
145+
)}`
147146
: spacer}
148147
</ExtensionInfo>
149148
</FinerDetails>

src/components/extensions-list.js

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { useState } from "react"
33
import Filters from "./filters/filters"
44
import ExtensionCard from "./extension-card"
55
import styled from "styled-components"
6-
import { extensionComparator } from "./util/extension-comparator"
6+
import { timestampExtensionComparator } from "./sortings/timestamp-extension-comparator"
7+
import Sortings from "./sortings/sortings"
78

89
const FilterableList = styled.div`
910
margin-left: var(--site-margins);
1011
margin-right: var(--site-margins);
11-
margin-top: 85px;
1212
display: flex;
1313
flex-direction: row;
1414
justify-content: space-between;
@@ -17,9 +17,9 @@ const FilterableList = styled.div`
1717
const Extensions = styled.ol`
1818
list-style: none;
1919
display: grid;
20+
gap: 30px;
2021
grid-template-columns: repeat(auto-fill, minmax(260px, auto));
2122
grid-template-rows: repeat(auto-fill, 1fr);
22-
width: 100%;
2323
`
2424

2525
const CardItem = styled.li`
@@ -28,6 +28,14 @@ const CardItem = styled.li`
2828
display: flex;
2929
`
3030

31+
const InfoSortRow = styled.div`
32+
margin-top: 85px;
33+
padding-left: var(--site-margins);
34+
padding-right: var(--site-margins);
35+
display: flex;
36+
justify-content: space-between;
37+
`
38+
3139
const RightColumn = styled.div`
3240
display: flex;
3341
flex-direction: column;
@@ -37,20 +45,20 @@ const RightColumn = styled.div`
3745

3846
const ExtensionCount = styled.h2`
3947
margin-top: 1.25rem;
40-
margin-left: 3.25rem;
4148
margin-bottom: 0.5rem;
4249
width: 100%;
43-
font-size: 1.25rem;
50+
font-size: 1rem;
4451
font-weight: 400;
52+
font-style: italic;
4553
`
4654

4755
const ExtensionsList = ({ extensions, categories }) => {
4856
// Do some pre-filtering for content we will never want, like superseded extensions
4957
const allExtensions = extensions.filter(extension => !extension.isSuperseded)
5058

5159
const [filteredExtensions, setExtensions] = useState(allExtensions)
60+
const [extensionComparator, setExtensionComparator] = useState(() => timestampExtensionComparator)
5261

53-
// TODO why is this guard necessary?
5462
if (allExtensions) {
5563
// Exclude unlisted extensions from the count, even though we sometimes show them if there's a direct search for it
5664
const extensionCount = allExtensions.filter(
@@ -65,26 +73,30 @@ const ExtensionsList = ({ extensions, categories }) => {
6573
: `Showing ${filteredExtensions.length} matching of ${extensionCount} extensions`
6674

6775
return (
68-
<FilterableList className="extensions-list">
69-
<Filters
70-
extensions={allExtensions}
71-
categories={categories}
72-
filterAction={setExtensions}
73-
/>
74-
<RightColumn>
75-
{" "}
76-
<ExtensionCount>{countMessage}</ExtensionCount>
77-
<Extensions>
78-
{filteredExtensions.map(extension => {
79-
return (
80-
<CardItem key={extension.id}>
81-
<ExtensionCard extension={extension} />
82-
</CardItem>
83-
)
84-
})}
85-
</Extensions>{" "}
86-
</RightColumn>
87-
</FilterableList>
76+
<div>
77+
<InfoSortRow><ExtensionCount>{countMessage}</ExtensionCount>
78+
<Sortings sorterAction={setExtensionComparator}></Sortings>
79+
</InfoSortRow>
80+
<FilterableList className="extensions-list">
81+
<Filters
82+
extensions={allExtensions}
83+
categories={categories}
84+
filterAction={setExtensions}
85+
/>
86+
<RightColumn>
87+
{" "}
88+
<Extensions>
89+
{filteredExtensions.map(extension => {
90+
return (
91+
<CardItem key={extension.id}>
92+
<ExtensionCard extension={extension} />
93+
</CardItem>
94+
)
95+
})}
96+
</Extensions>{" "}
97+
</RightColumn>
98+
</FilterableList>
99+
</div>
88100
)
89101
} else {
90102
return (
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const HOURS_IN_MS = 60 * 60 * 1000
2+
3+
function compareByTimestamp(a, b) {
4+
const timestampA = roundToTheNearestHour(a?.metadata?.maven?.timestamp)
5+
const timestampB = roundToTheNearestHour(b?.metadata?.maven?.timestamp)
6+
7+
if (timestampA && timestampB) {
8+
const delta = timestampB - timestampA
9+
if (delta === 0) {
10+
return compareAlphabetically(a, b)
11+
} else {
12+
return delta
13+
}
14+
} else if (timestampA) {
15+
return -1
16+
} else if (timestampB) {
17+
return 1
18+
}
19+
}
20+
21+
const alphabeticalExtensionComparator = (a, b) => {
22+
23+
let comp = compareAlphabetically(a, b)
24+
if (comp === 0) {
25+
comp = compareByTimestamp(a, b)
26+
}
27+
return comp
28+
}
29+
30+
function roundToTheNearestHour(n) {
31+
if (n) {
32+
return Math.round(n / HOURS_IN_MS) * HOURS_IN_MS
33+
}
34+
}
35+
36+
function compareAlphabetically(a, b) {
37+
if (a.sortableName) {
38+
return a.sortableName.localeCompare(b.sortableName)
39+
} else if (b.sortableName) {
40+
return 1
41+
} else {
42+
return 0
43+
}
44+
}
45+
46+
module.exports = { alphabeticalExtensionComparator }
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { alphabeticalExtensionComparator } from "./alphabetical-extension-comparator"
2+
3+
describe("the alphabetical extension comparator", () => {
4+
it("sorts by date when the names are identical", () => {
5+
const a = { sortableName: "alpha", metadata: { maven: { timestamp: 1795044005 } } }
6+
const b = { sortableName: "alpha", metadata: { maven: { timestamp: 1695044005 } } }
7+
8+
expect(alphabeticalExtensionComparator(a, b)).toBeLessThan(0)
9+
expect(alphabeticalExtensionComparator(b, a)).toBeGreaterThan(0)
10+
})
11+
12+
it("put extensions with a name ahead of those without", () => {
13+
const a = { sortableName: "alpha" }
14+
const b = {}
15+
16+
expect(alphabeticalExtensionComparator(a, b)).toBe(-1)
17+
expect(alphabeticalExtensionComparator(b, a)).toBe(1)
18+
})
19+
20+
it("sorts alphabetically", () => {
21+
const a = { sortableName: "alpha", metadata: { maven: { timestamp: 1795044005 } } }
22+
const b = { sortableName: "beta", metadata: { maven: { timestamp: 1695044005 } } }
23+
24+
expect(alphabeticalExtensionComparator(a, b)).toBeLessThan(0)
25+
expect(alphabeticalExtensionComparator(b, a)).toBeGreaterThan(0)
26+
})
27+
28+
})
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import * as React from "react"
2+
import styled from "styled-components"
3+
import Select from "react-select"
4+
import { styles } from "../util/styles/style"
5+
import { timestampExtensionComparator } from "./timestamp-extension-comparator"
6+
import { alphabeticalExtensionComparator } from "./alphabetical-extension-comparator"
7+
8+
const Title = styled.label`
9+
font-size: var(--font-size-16);
10+
letter-spacing: 0;
11+
color: var(--grey-2);
12+
width: 100px;
13+
text-align: right;
14+
`
15+
16+
const SortBar = styled.div`
17+
display: flex;
18+
flex-direction: row;
19+
justify-content: flex-end;
20+
align-items: center;
21+
gap: var(--a-small-space);
22+
`
23+
24+
const Element = styled.form`
25+
display: flex;
26+
flex-direction: column;
27+
justify-content: flex-start;
28+
align-items: flex-start;
29+
gap: 16px;
30+
`
31+
32+
// Grab CSS variables in javascript
33+
const grey = styles["grey-2"]
34+
35+
const colourStyles = {
36+
control: styles => ({
37+
...styles,
38+
borderRadius: 0,
39+
color: grey,
40+
borderColor: grey,
41+
width: "280px",
42+
}),
43+
option: (styles, { isDisabled }) => {
44+
return {
45+
...styles,
46+
cursor: isDisabled ? "not-allowed" : "default",
47+
borderRadius: 0,
48+
}
49+
},
50+
dropdownIndicator: styles => ({
51+
...styles,
52+
color: grey, // Custom colour
53+
}),
54+
indicatorSeparator: styles => ({
55+
...styles,
56+
margin: 0,
57+
backgroundColor: grey,
58+
}),
59+
}
60+
61+
const sortings = [
62+
{ label: "most recently released", value: "time", comparator: timestampExtensionComparator },
63+
{ label: "alphabetical", value: "alpha", comparator: alphabeticalExtensionComparator }]
64+
65+
const Sortings = ({ sorterAction }) => {
66+
67+
const setSortByDescription = (entry) => {
68+
// We need to wrap our comparator functions in functions or they get called, which goes very badly
69+
sorterAction && sorterAction(() => entry.comparator)
70+
}
71+
72+
return (
73+
<SortBar className="sortings">
74+
<Title htmlFor="sort">Sort by</Title>
75+
<Element data-testid="sort-form">
76+
<Select
77+
placeholder="default"
78+
options={sortings}
79+
onChange={label => setSortByDescription(label)}
80+
name="sort"
81+
inputId="sort"
82+
styles={colourStyles}
83+
/>
84+
</Element>
85+
</SortBar>
86+
)
87+
}
88+
89+
export default Sortings
90+
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from "react"
2+
import { render, screen } from "@testing-library/react"
3+
import userEvent from "@testing-library/user-event"
4+
import Sortings from "./sortings"
5+
import selectEvent from "react-select-event"
6+
import { alphabeticalExtensionComparator } from "./alphabetical-extension-comparator"
7+
import { timestampExtensionComparator } from "./timestamp-extension-comparator"
8+
9+
10+
let mockQueryParamSearchString = undefined
11+
12+
jest.mock("react-use-query-param-string", () => {
13+
14+
const original = jest.requireActual("react-use-query-param-string")
15+
return {
16+
...original,
17+
useQueryParamString: jest.fn().mockImplementation(() => [mockQueryParamSearchString, jest.fn().mockImplementation((val) => mockQueryParamSearchString = val), true]),
18+
getQueryParams: jest.fn().mockReturnValue({ "search-regex": mockQueryParamSearchString })
19+
20+
}
21+
})
22+
23+
describe("sorting bar", () => {
24+
const sortListener = jest.fn()
25+
26+
beforeEach(() => {
27+
mockQueryParamSearchString = undefined
28+
render(
29+
<Sortings
30+
sorterAction={sortListener}
31+
/>
32+
)
33+
})
34+
35+
afterEach(() => {
36+
jest.clearAllMocks()
37+
})
38+
39+
40+
describe("sorting", () => {
41+
userEvent.setup()
42+
const label = "Sort by"
43+
44+
it("lets the listener know when a new sort scheme is chosen", async () => {
45+
expect(screen.getByTestId("sort-form")).toHaveFormValues({
46+
"sort": "",
47+
})
48+
await selectEvent.select(screen.getByLabelText(label), "most recently released")
49+
50+
expect(sortListener).toHaveBeenCalled()
51+
})
52+
53+
it("lets the listener know when a new timestamp sort scheme is chosen", async () => {
54+
expect(screen.getByTestId("sort-form")).toHaveFormValues({
55+
"sort": "",
56+
})
57+
await selectEvent.select(screen.getByLabelText(label), "most recently released")
58+
59+
expect(sortListener).toHaveBeenCalledWith(expect.any(Function))
60+
const param = sortListener.mock.calls[0][0]
61+
expect(param()).toEqual(timestampExtensionComparator)
62+
})
63+
64+
it("lets the listener know when an alphabetical scheme is chosen", async () => {
65+
expect(screen.getByTestId("sort-form")).toHaveFormValues({
66+
"sort": "",
67+
})
68+
await selectEvent.select(screen.getByLabelText(label), "alphabetical")
69+
70+
expect(sortListener).toHaveBeenCalledWith(expect.any(Function))
71+
const param = sortListener.mock.calls[0][0]
72+
expect(param()).toEqual(alphabeticalExtensionComparator)
73+
})
74+
75+
})
76+
})

0 commit comments

Comments
 (0)