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 + +