diff --git a/frontend/__tests__/SoftwareIndex.test.tsx b/frontend/__tests__/SoftwareIndex.test.tsx
deleted file mode 100644
index d0cba5d18..000000000
--- a/frontend/__tests__/SoftwareIndex.test.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2022 - 2023 dv4all
-//
-// SPDX-License-Identifier: Apache-2.0
-
-import {render, screen} from '@testing-library/react'
-import SoftwareIndexPage from '../pages/software/index'
-import {WrappedComponentWithProps} from '../utils/jest/WrappedComponents'
-import {mockResolvedValue} from '../utils/jest/mockFetch'
-
-// mock fetch response
-import softwareItem from './__mocks__/softwareItem.json'
-import {RsdUser} from '../auth'
-
-const mockedResponse=[softwareItem]
-
-describe('pages/software/index.tsx', () => {
- beforeEach(() => {
- mockResolvedValue(mockedResponse, {
- status:206,
- headers:{
- // mock getting Content-Range from the header
- get:()=>'0-11/200'
- },
- statusText:'OK',
- })
- })
-
- it('renders heading with the title Software', async() => {
- render(WrappedComponentWithProps(
- SoftwareIndexPage, {
- props: {
- count:200,
- page:0,
- rows:12,
- software:mockedResponse,
- tags:[],
- },
- // user session
- session:{
- status: 'missing',
- token: 'test-token',
- user: {name:'Test user'} as RsdUser
- }
- }
- ))
- const heading = await screen.findByRole('heading',{
- name: 'Software'
- })
- expect(heading).toBeInTheDocument()
- expect(heading.innerHTML).toEqual('Software')
- })
- it('renders software card title',async()=>{
- render(WrappedComponentWithProps(
- SoftwareIndexPage, {
- props: {
- count:200,
- page:0,
- rows:12,
- software:mockedResponse,
- tags:[]
- },
- // user session
- session:{
- status: 'missing',
- token: 'test-token',
- user: {name:'Test user'} as RsdUser
- }
- }
- ))
- const cardTitle = mockedResponse[0].brand_name
- const card = await screen.findByText(cardTitle)
- expect(card).toBeInTheDocument()
- })
-})
diff --git a/frontend/__tests__/SoftwareOverview.test.tsx b/frontend/__tests__/SoftwareOverview.test.tsx
new file mode 100644
index 000000000..efd1a9204
--- /dev/null
+++ b/frontend/__tests__/SoftwareOverview.test.tsx
@@ -0,0 +1,126 @@
+// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2022 - 2023 dv4all
+//
+// SPDX-License-Identifier: Apache-2.0
+
+import {render, screen, within} from '@testing-library/react'
+import {WithAppContext} from '~/utils/jest/WithAppContext'
+
+import SoftwareOverviewPage from '../pages/software/index'
+import {LayoutType} from '~/components/software/overview/SearchSection'
+
+// mocked data & props
+import mockData from './__mocks__/softwareOverviewData.json'
+const mockProps = {
+ search:null,
+ keywords:null,
+ prog_lang: null,
+ licenses:null,
+ order:null,
+ page: 1,
+ rows: 12,
+ count: 408,
+ layout: 'masonry' as LayoutType,
+ keywordsList: mockData.keywordsList,
+ languagesList: mockData.languagesList,
+ licensesList: mockData.licensesList,
+ software: mockData.software as any,
+ highlights: mockData.highlights as any
+}
+
+describe('pages/software/index.tsx', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it('renders title All software', () => {
+ render(
+
+
+
+ )
+ const heading = screen.getByRole('heading',{
+ name: 'All software'
+ })
+ expect(heading).toBeInTheDocument()
+ })
+ it('renders highlights section with all (5) items', () => {
+ render(
+
+
+
+ )
+ const carousel = screen.getByTestId('highlights-carousel')
+ const cards = screen.getAllByTestId('highlights-card')
+ expect(cards.length).toEqual(mockData.highlights.length)
+ })
+
+ it('renders software filter panel with orderBy and 3 filters (combobox)', () => {
+ render(
+
+
+
+ )
+ // get reference to filter panel
+ const panel = screen.getByTestId('software-filters-panel')
+ // find order by testid
+ const order = within(panel).getByTestId('filters-order-by')
+ // should have 3 filters
+ const filters = within(panel).getAllByRole('combobox')
+ expect(filters.length).toEqual(3)
+ // screen.debug(filters)
+ })
+
+ it('renders searchbox with placeholder Find software', async () => {
+ render(
+
+
+
+ )
+ screen.getByPlaceholderText('Find software')
+ })
+
+ it('renders layout options (toggle button group)', async () => {
+ mockProps.layout='masonry'
+ render(
+
+
+
+ )
+ const buttonGroup = screen.getByTestId('card-layout-options')
+ })
+
+ it('renders (12) masonry cards', async () => {
+ mockProps.layout='masonry'
+ render(
+
+
+
+ )
+ const cards = screen.getAllByTestId('software-masonry-card')
+ expect(cards.length).toEqual(mockProps.software.length)
+ })
+
+ it('renders (12) grid cards', async () => {
+ mockProps.layout='grid'
+ render(
+
+
+
+ )
+ const cards = screen.getAllByTestId('software-grid-card')
+ expect(cards.length).toEqual(mockProps.software.length)
+ })
+
+ it('renders (12) list items', async () => {
+ mockProps.layout='list'
+ render(
+
+
+
+ )
+ const cards = screen.getAllByTestId('software-list-item')
+ expect(cards.length).toEqual(mockProps.software.length)
+ })
+
+})
diff --git a/frontend/__tests__/__mocks__/softwareOverviewData.json b/frontend/__tests__/__mocks__/softwareOverviewData.json
new file mode 100644
index 000000000..699c81f1f
--- /dev/null
+++ b/frontend/__tests__/__mocks__/softwareOverviewData.json
@@ -0,0 +1,497 @@
+{
+ "keywordsList":[
+ {
+ "keyword": "Big data",
+ "keyword_cnt": 46
+ },
+ {
+ "keyword": "GPU",
+ "keyword_cnt": 33
+ },
+ {
+ "keyword": "High performance computing",
+ "keyword_cnt": 23
+ },
+ {
+ "keyword": "Image processing",
+ "keyword_cnt": 33
+ },
+ {
+ "keyword": "Inter-operability & linked data",
+ "keyword_cnt": 36
+ },
+ {
+ "keyword": "Machine learning",
+ "keyword_cnt": 17
+ },
+ {
+ "keyword": "Multi-scale & multi model simulations",
+ "keyword_cnt": 32
+ },
+ {
+ "keyword": "Optimized data handling",
+ "keyword_cnt": 38
+ },
+ {
+ "keyword": "Real time data analysis",
+ "keyword_cnt": 37
+ },
+ {
+ "keyword": "Text analysis & natural language processing",
+ "keyword_cnt": 33
+ },
+ {
+ "keyword": "Visualization",
+ "keyword_cnt": 38
+ },
+ {
+ "keyword": "Workflow technologies",
+ "keyword_cnt": 31
+ }
+ ],
+ "languagesList":[
+ {
+ "prog_language": "C++",
+ "prog_language_cnt": 1
+ },
+ {
+ "prog_language": "CMake",
+ "prog_language_cnt": 1
+ },
+ {
+ "prog_language": "CSS",
+ "prog_language_cnt": 4
+ },
+ {
+ "prog_language": "Dart",
+ "prog_language_cnt": 1
+ },
+ {
+ "prog_language": "Dockerfile",
+ "prog_language_cnt": 9
+ },
+ {
+ "prog_language": "GLSL",
+ "prog_language_cnt": 1
+ },
+ {
+ "prog_language": "Go",
+ "prog_language_cnt": 3
+ },
+ {
+ "prog_language": "HTML",
+ "prog_language_cnt": 6
+ },
+ {
+ "prog_language": "Java",
+ "prog_language_cnt": 1
+ },
+ {
+ "prog_language": "JavaScript",
+ "prog_language_cnt": 4
+ },
+ {
+ "prog_language": "Jinja",
+ "prog_language_cnt": 2
+ },
+ {
+ "prog_language": "Jupyter Notebook",
+ "prog_language_cnt": 2
+ },
+ {
+ "prog_language": "Makefile",
+ "prog_language_cnt": 4
+ },
+ {
+ "prog_language": "PLpgSQL",
+ "prog_language_cnt": 1
+ },
+ {
+ "prog_language": "Python",
+ "prog_language_cnt": 5
+ },
+ {
+ "prog_language": "R",
+ "prog_language_cnt": 4
+ },
+ {
+ "prog_language": "Ruby",
+ "prog_language_cnt": 3
+ },
+ {
+ "prog_language": "Shell",
+ "prog_language_cnt": 9
+ },
+ {
+ "prog_language": "Swift",
+ "prog_language_cnt": 1
+ },
+ {
+ "prog_language": "TeX",
+ "prog_language_cnt": 3
+ },
+ {
+ "prog_language": "TypeScript",
+ "prog_language_cnt": 1
+ }
+ ],
+ "licensesList":[
+ {
+ "license": "Apache-2.0",
+ "license_cnt": 68
+ },
+ {
+ "license": "CC-BY-4.0",
+ "license_cnt": 67
+ },
+ {
+ "license": "CC-BY-NC-ND-3.0",
+ "license_cnt": 81
+ },
+ {
+ "license": "GPL-2.0-or-later",
+ "license_cnt": 68
+ },
+ {
+ "license": "LGPL-2.0-or-later",
+ "license_cnt": 72
+ },
+ {
+ "license": "MIT",
+ "license_cnt": 84
+ }
+ ],
+ "software":[
+ {
+ "id": "bdf9dd4d-46f7-42b7-87e1-173857b9238f",
+ "slug": "software-avon-sausages-dynamic",
+ "brand_name": "Software: Avon Sausages dynamic",
+ "short_statement": "Andy shoes are designed to keeping in mind durability as well as trends, the most stylish range of shoes & sandals",
+ "image_id": "b5f66923d5533c42f87cfcac398275705b13e59d",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 1,
+ "mention_cnt": 4,
+ "is_published": true,
+ "keywords": [
+ "Text analysis & natural language processing"
+ ],
+ "keywords_text": "Text analysis & natural language processing",
+ "prog_lang": null,
+ "licenses": [
+ "MIT",
+ "Apache-2.0"
+ ]
+ },
+ {
+ "id": "f2da6206-e693-49b8-a33a-5f5963ccb5bf",
+ "slug": "software-override-mazda-data",
+ "brand_name": "Software: override Mazda Data",
+ "short_statement": "New ABC 13 9370, 13.3, 5th Gen CoreA5-8250U, 8GB RAM, 256GB SSD, power UHD Graphics, OS 10 Home, OS Office A & J 2016",
+ "image_id": "425a1fac3db6f89d5b258cb5a0bf0bbeaa31159f",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 3,
+ "mention_cnt": 3,
+ "is_published": true,
+ "keywords": [
+ "Visualization"
+ ],
+ "keywords_text": "Visualization",
+ "prog_lang": null,
+ "licenses": null
+ },
+ {
+ "id": "7da44697-4227-4088-b78d-4d52ee177ab4",
+ "slug": "real-software-hmph-virginia-armenian",
+ "brand_name": "Real software: hmph Virginia Armenian",
+ "short_statement": "Ergonomic executive chair upholstered in bonded black leather and PVC padded seat and back for all-day comfort and support",
+ "image_id": "3fbd02ad27d9c3d11f90431c4ff48f94bb9b9af2",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 2,
+ "mention_cnt": 1,
+ "is_published": true,
+ "keywords": null,
+ "keywords_text": null,
+ "prog_lang": null,
+ "licenses": [
+ "LGPL-2.0-or-later",
+ "CC-BY-NC-ND-3.0"
+ ]
+ },
+ {
+ "id": "542e8504-5a07-46d1-a931-b1d048024368",
+ "slug": "real-software-woman",
+ "brand_name": "Real software: woman",
+ "short_statement": "The Football Is Good For Training And Recreational Purposes",
+ "image_id": "b5f66923d5533c42f87cfcac398275705b13e59d",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 2,
+ "mention_cnt": 3,
+ "is_published": true,
+ "keywords": null,
+ "keywords_text": null,
+ "prog_lang": null,
+ "licenses": [
+ "CC-BY-4.0"
+ ]
+ },
+ {
+ "id": "bf5b70f1-0df8-45dc-aa5b-7b3055a0faf8",
+ "slug": "real-software-berkshire-buckinghamshire-policy",
+ "brand_name": "Real software: Berkshire Buckinghamshire policy",
+ "short_statement": "Ergonomic executive chair upholstered in bonded black leather and PVC padded seat and back for all-day comfort and support",
+ "image_id": "795cb8f47ea710c8c138adec049bea9c98246149",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 2,
+ "mention_cnt": 8,
+ "is_published": true,
+ "keywords": [
+ "Real time data analysis"
+ ],
+ "keywords_text": "Real time data analysis",
+ "prog_lang": null,
+ "licenses": [
+ "MIT",
+ "CC-BY-NC-ND-3.0"
+ ]
+ },
+ {
+ "id": "5b18576d-3ef2-4ad3-9742-d2ada295bca8",
+ "slug": "real-software-gasoline-magenta",
+ "brand_name": "Real software: Gasoline magenta",
+ "short_statement": "The Apollotech B340 is an affordable wireless mouse with reliable connectivity, 12 months battery life and modern design",
+ "image_id": "b80a775f137291e3298bd0c2585fa9717db9fc7c",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 3,
+ "mention_cnt": 2,
+ "is_published": true,
+ "keywords": [
+ "High performance computing"
+ ],
+ "keywords_text": "High performance computing",
+ "prog_lang": null,
+ "licenses": [
+ "CC-BY-NC-ND-3.0"
+ ]
+ },
+ {
+ "id": "68bc6c6b-2c7f-4f29-9066-1cac93b54e2e",
+ "slug": "real-software-female-gah-capitulation-southwest-road-avon-mole-georgia-missouri-connect-checking-ascii-gastonia-country-grocery-pink-world-css-smart",
+ "brand_name": "Real software: female gah capitulation Southwest Road Avon mole Georgia Missouri connect Checking ASCII Gastonia Country Grocery pink World CSS Smart",
+ "short_statement": "The Football Is Good For Training And Recreational Purposes",
+ "image_id": "1727af42cf0deb5fcc30befca450d1a3d4927eab",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 1,
+ "mention_cnt": 10,
+ "is_published": true,
+ "keywords": null,
+ "keywords_text": null,
+ "prog_lang": null,
+ "licenses": [
+ "LGPL-2.0-or-later",
+ "CC-BY-NC-ND-3.0"
+ ]
+ },
+ {
+ "id": "6da8bfe5-2a98-4071-9546-996903da48fa",
+ "slug": "real-software-bandwidth",
+ "brand_name": "Real software: bandwidth",
+ "short_statement": "The automobile layout consists of a front-engine design, with transaxle-type transmissions mounted at the rear of the engine and four wheel drive",
+ "image_id": "a27c82df7e1d2b13757ede271dd652c57d37d5d1",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": null,
+ "mention_cnt": 4,
+ "is_published": true,
+ "keywords": [
+ "Big data",
+ "Workflow technologies"
+ ],
+ "keywords_text": "Big data Workflow technologies",
+ "prog_lang": null,
+ "licenses": [
+ "GPL-2.0-or-later"
+ ]
+ },
+ {
+ "id": "8fd29445-963e-49c7-b5e3-c33f6fdc241b",
+ "slug": "real-software-borders-maxime",
+ "brand_name": "Real software: Borders maxime",
+ "short_statement": "Carbonite web goalkeeper gloves are ergonomically designed to give easy fit",
+ "image_id": "0734d6a1e1e8e4e99c9d3d78c6fd635c3e41699f",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 3,
+ "mention_cnt": 8,
+ "is_published": true,
+ "keywords": [
+ "GPU",
+ "Optimized data handling"
+ ],
+ "keywords_text": "GPU Optimized data handling",
+ "prog_lang": null,
+ "licenses": [
+ "Apache-2.0"
+ ]
+ },
+ {
+ "id": "04e967b5-0de5-450b-8fc6-99dccf526953",
+ "slug": "real-software-salmon-minivan-unbranded-sedan",
+ "brand_name": "Real software: salmon Minivan Unbranded Sedan",
+ "short_statement": "The Football Is Good For Training And Recreational Purposes",
+ "image_id": "786e0df3a302c9e02b36710ee634a7c49f5b4d2a",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 1,
+ "mention_cnt": 2,
+ "is_published": true,
+ "keywords": [
+ "GPU"
+ ],
+ "keywords_text": "GPU",
+ "prog_lang": null,
+ "licenses": null
+ },
+ {
+ "id": "48fd3afc-65dc-4f6b-ab61-662db9eb97f8",
+ "slug": "real-software-incredible-indexing-violet",
+ "brand_name": "Real software: Incredible indexing violet",
+ "short_statement": "New range of formal shirts are designed keeping you in mind. With fits and styling that will make you stand apart",
+ "image_id": "3fbd02ad27d9c3d11f90431c4ff48f94bb9b9af2",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 4,
+ "mention_cnt": 1,
+ "is_published": true,
+ "keywords": null,
+ "keywords_text": null,
+ "prog_lang": [
+ "R"
+ ],
+ "licenses": [
+ "CC-BY-NC-ND-3.0",
+ "MIT"
+ ]
+ },
+ {
+ "id": "b0e51739-bbd5-4a70-8639-38ad0bf76c16",
+ "slug": "real-software-southeast-southwest-copy-zirconium",
+ "brand_name": "Real software: Southeast Southwest copy Zirconium",
+ "short_statement": "The automobile layout consists of a front-engine design, with transaxle-type transmissions mounted at the rear of the engine and four wheel drive",
+ "image_id": "8ac56d71326e8acc332de83a585f7b87e111957c",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "contributor_cnt": 2,
+ "mention_cnt": 10,
+ "is_published": true,
+ "keywords": null,
+ "keywords_text": null,
+ "prog_lang": [
+ "Python"
+ ],
+ "licenses": [
+ "MIT",
+ "LGPL-2.0-or-later"
+ ]
+ }
+ ],
+ "highlights":[
+ {
+ "id": "20a59ce6-96fc-4257-81ab-d235d73822a0",
+ "slug": "real-software-facilitate-evolve-uganda",
+ "brand_name": "Real software: facilitate evolve Uganda",
+ "short_statement": "New ABC 13 9370, 13.3, 5th Gen CoreA5-8250U, 8GB RAM, 256GB SSD, power UHD Graphics, OS 10 Home, OS Office A & J 2016",
+ "image_id": "6404fe74f01e90845f37f5e8f36c914596de48c5",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "is_published": true,
+ "contributor_cnt": 5,
+ "mention_cnt": 10,
+ "keywords": [
+ "GPU",
+ "Inter-operability & linked data"
+ ],
+ "keywords_text": "GPU Inter-operability & linked data",
+ "prog_lang": [
+ "Go",
+ "Ruby",
+ "Shell",
+ "Makefile",
+ "Dockerfile"
+ ],
+ "licenses": null,
+ "position": 1
+ },
+ {
+ "id": "cd5bb638-32af-494b-b00d-0c28ccc4ba7f",
+ "slug": "real-software-sticky-phosphorus-json-enable",
+ "brand_name": "Real software: sticky Phosphorus JSON enable",
+ "short_statement": "The beautiful range of Apple Naturalé that has an exciting mix of natural ingredients. With the Goodness of 100% Natural Ingredients",
+ "image_id": "f3921426c70da70364e528bca17ecce65721120b",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "is_published": true,
+ "contributor_cnt": 5,
+ "mention_cnt": 10,
+ "keywords": null,
+ "keywords_text": null,
+ "prog_lang": null,
+ "licenses": [
+ "CC-BY-4.0",
+ "CC-BY-NC-ND-3.0"
+ ],
+ "position": 2
+ },
+ {
+ "id": "eb259590-bedd-4e41-9cff-b8f168752391",
+ "slug": "real-software-utilize-orange-female-actinium",
+ "brand_name": "Real software: utilize orange female Actinium",
+ "short_statement": "The Nagasaki Lander is the trademarked name of several series of Nagasaki sport bikes, that started with the 1984 ABC800J",
+ "image_id": "b5f66923d5533c42f87cfcac398275705b13e59d",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "is_published": true,
+ "contributor_cnt": 4,
+ "mention_cnt": 10,
+ "keywords": [
+ "Inter-operability & linked data"
+ ],
+ "keywords_text": "Inter-operability & linked data",
+ "prog_lang": null,
+ "licenses": null,
+ "position": 3
+ },
+ {
+ "id": "873bee1f-afd4-4e94-a0b2-8b3bf57392cc",
+ "slug": "real-software-rustic-into-buckinghamshire",
+ "brand_name": "Real software: Rustic into Buckinghamshire",
+ "short_statement": "The Apollotech B340 is an affordable wireless mouse with reliable connectivity, 12 months battery life and modern design",
+ "image_id": "b80a775f137291e3298bd0c2585fa9717db9fc7c",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "is_published": true,
+ "contributor_cnt": 2,
+ "mention_cnt": 10,
+ "keywords": [
+ "Inter-operability & linked data"
+ ],
+ "keywords_text": "Inter-operability & linked data",
+ "prog_lang": null,
+ "licenses": [
+ "GPL-2.0-or-later"
+ ],
+ "position": 4
+ },
+ {
+ "id": "b0e51739-bbd5-4a70-8639-38ad0bf76c16",
+ "slug": "real-software-southeast-southwest-copy-zirconium",
+ "brand_name": "Real software: Southeast Southwest copy Zirconium",
+ "short_statement": "The automobile layout consists of a front-engine design, with transaxle-type transmissions mounted at the rear of the engine and four wheel drive",
+ "image_id": "8ac56d71326e8acc332de83a585f7b87e111957c",
+ "updated_at": "2023-05-11T16:29:51.35019+00:00",
+ "is_published": true,
+ "contributor_cnt": 2,
+ "mention_cnt": 10,
+ "keywords": null,
+ "keywords_text": null,
+ "prog_lang": [
+ "Python"
+ ],
+ "licenses": [
+ "MIT",
+ "LGPL-2.0-or-later"
+ ],
+ "position": 5
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/__tests__/__mocks__/softwareOverviewData.json.license b/frontend/__tests__/__mocks__/softwareOverviewData.json.license
new file mode 100644
index 000000000..3bb7d18c2
--- /dev/null
+++ b/frontend/__tests__/__mocks__/softwareOverviewData.json.license
@@ -0,0 +1,4 @@
+SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
+SPDX-FileCopyrightText: 2022 - 2023 dv4all
+
+SPDX-License-Identifier: Apache-2.0
diff --git a/frontend/components/cards/CardTitleSubtitle.tsx b/frontend/components/cards/CardTitleSubtitle.tsx
new file mode 100644
index 000000000..a3bb980fc
--- /dev/null
+++ b/frontend/components/cards/CardTitleSubtitle.tsx
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2023 dv4all
+//
+// SPDX-License-Identifier: Apache-2.0
+
+type CardTitleSubtitleProps = {
+ title: string
+ subtitle:string
+}
+
+export default function CardTitleSubtitle({title,subtitle}:CardTitleSubtitleProps) {
+ return (
+ <>
+
+ {title}
+
+
+ >
+ )
+}
diff --git a/frontend/components/software/overview/KeywordList.tsx b/frontend/components/cards/KeywordList.tsx
similarity index 100%
rename from frontend/components/software/overview/KeywordList.tsx
rename to frontend/components/cards/KeywordList.tsx
diff --git a/frontend/components/software/overview/ProgrammingLanguageList.tsx b/frontend/components/cards/ProgrammingLanguageList.tsx
similarity index 100%
rename from frontend/components/software/overview/ProgrammingLanguageList.tsx
rename to frontend/components/cards/ProgrammingLanguageList.tsx
diff --git a/frontend/components/software/overview/SoftwareMetrics.tsx b/frontend/components/cards/SoftwareMetrics.tsx
similarity index 95%
rename from frontend/components/software/overview/SoftwareMetrics.tsx
rename to frontend/components/cards/SoftwareMetrics.tsx
index 4efb7e837..4ecf36ab6 100644
--- a/frontend/components/software/overview/SoftwareMetrics.tsx
+++ b/frontend/components/cards/SoftwareMetrics.tsx
@@ -31,7 +31,7 @@ export default function SoftwareMetrics({contributor_cnt,mention_cnt,downloads}:
{(downloads && downloads > 0) &&
- 34K
+ {downloads}
}
diff --git a/frontend/components/layout/ImageWithPlaceholder.tsx b/frontend/components/layout/ImageWithPlaceholder.tsx
index 687ddffbc..29efbcd9c 100644
--- a/frontend/components/layout/ImageWithPlaceholder.tsx
+++ b/frontend/components/layout/ImageWithPlaceholder.tsx
@@ -12,22 +12,33 @@ export type ImageWithPlaceholderProps = {
bgSize?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down',
bgPosition?: string
placeholder?: string
+ width?: string
+ height?: string
+ type?: 'gradient' | 'icon'
}
export default function ImageWithPlaceholder({
- src, alt, className, bgSize = 'contain', bgPosition = 'center', placeholder
+ src, alt, className, bgSize = 'contain', bgPosition = 'center', placeholder,
+ width = '4rem', height = '4rem', type='icon'
}: ImageWithPlaceholderProps
) {
if (!src) {
+ if (type === 'gradient') {
+ return (
+
+ )
+ }
return (
{placeholder}
diff --git a/frontend/components/mention/MentionNote.tsx b/frontend/components/mention/MentionNote.tsx
index ac83ea50c..881aea638 100644
--- a/frontend/components/mention/MentionNote.tsx
+++ b/frontend/components/mention/MentionNote.tsx
@@ -1,11 +1,11 @@
-// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2022 dv4all
+// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2022 - 2023 dv4all
//
// SPDX-License-Identifier: Apache-2.0
export default function MentionNote({note}: { note: string | null }) {
if (note) {
- return
{note}
+ return
{note}
}
return null
}
diff --git a/frontend/components/software/highlights/HighlightsCard.tsx b/frontend/components/software/highlights/HighlightsCard.tsx
new file mode 100644
index 000000000..643e9c738
--- /dev/null
+++ b/frontend/components/software/highlights/HighlightsCard.tsx
@@ -0,0 +1,73 @@
+// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all)
+// SPDX-FileCopyrightText: 2023 dv4all
+//
+// SPDX-License-Identifier: Apache-2.0
+
+import Link from 'next/link'
+import {SoftwareListItem} from '~/types/SoftwareTypes'
+import {getImageUrl} from '~/utils/editImage'
+import KeywordList from '~/components/cards/KeywordList'
+import CardTitleSubtitle from '~/components/cards/CardTitleSubtitle'
+import ProgrammingLanguageList from '~/components/cards//ProgrammingLanguageList'
+import SoftwareMetrics from '~/components/cards/SoftwareMetrics'
+
+type SoftwareCardProps = {
+ item: SoftwareListItem
+}
+
+export default function HighlightsCard({item}:SoftwareCardProps){
+
+ const visibleNumberOfKeywords: number = 3
+ const visibleNumberOfProgLang: number = 3
+
+ return (
+
+
+ {/* Cover image */}
+ {
+ item.image_id &&
+
+ }
+
+ {/* Card content */}
+
+
+
+ {/* keywords */}
+
+
+
+
+
+ {/* Languages */}
+
+ {/* Metrics */}
+
+
+
+
+
+ )
+}
diff --git a/frontend/components/software/highlights/HighlightsCarousel.tsx b/frontend/components/software/highlights/HighlightsCarousel.tsx
index f8c6db136..5cddaa69d 100644
--- a/frontend/components/software/highlights/HighlightsCarousel.tsx
+++ b/frontend/components/software/highlights/HighlightsCarousel.tsx
@@ -5,11 +5,13 @@
import {UIEventHandler, useRef, useState} from 'react'
import {SoftwareHighlight} from '~/components/admin/software-highlights/apiSoftwareHighlights'
-import {SoftwareCard} from '~/components/software/overview/SoftwareCard'
+import LeftButton from './LeftButton'
+import RightButton from './RightButton'
+import HighlightsCard from './HighlightsCard'
export const HighlightsCarousel = ({items=[]}: {items:SoftwareHighlight[]}) => {
-
- const canrdMovement: number = 680 // card size + margin
+ // card size + margin
+ const cardMovement: number = 680
// Keep track of the current scroll position of the carousel.
const [scrollPosition, setScrollPosition] = useState(0)
const carousel = useRef
(null)
@@ -18,13 +20,13 @@ export const HighlightsCarousel = ({items=[]}: {items:SoftwareHighlight[]}) => {
const handleNextClick = () => {
// move the scroll to the left
if (carousel.current) {
- carousel.current.scrollLeft -= canrdMovement
+ carousel.current.scrollLeft += cardMovement
}
}
const handlePrevClick = () => {
if (carousel.current) {
- carousel.current.scrollLeft += canrdMovement
+ carousel.current.scrollLeft -= cardMovement
}
}
@@ -34,50 +36,27 @@ export const HighlightsCarousel = ({items=[]}: {items:SoftwareHighlight[]}) => {
}
return (
-
+
{/* Left Button */}
{scrollPosition > 0 &&
-
-
-
+
}
-
{/* Right Button */}
-
-
-
-
+
{/* Carousel */}
+ className="flex gap-4 snap-start sm:snap-none scroll-smooth overflow-x-scroll scrollbar-hide p-4 sm:pr-[600px] lg:pl-[100px] 2xl:pl-[400px]"
+ style={{scrollbarWidth:'none',left:-scrollPosition}}>
{/* render software card in the row direction */}
{items.map(highlight => (
-
+
))
}
diff --git a/frontend/components/software/highlights/LeftButton.tsx b/frontend/components/software/highlights/LeftButton.tsx
new file mode 100644
index 000000000..5e91b7e2b
--- /dev/null
+++ b/frontend/components/software/highlights/LeftButton.tsx
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2023 dv4all
+//
+// SPDX-License-Identifier: Apache-2.0
+
+export default function LeftButton({handlePrevClick}:{handlePrevClick:()=>void}) {
+ return (
+
+
+
+ )
+}
diff --git a/frontend/components/software/highlights/RightButton.tsx b/frontend/components/software/highlights/RightButton.tsx
new file mode 100644
index 000000000..fdfcf0bfb
--- /dev/null
+++ b/frontend/components/software/highlights/RightButton.tsx
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2023 dv4all
+//
+// SPDX-License-Identifier: Apache-2.0
+
+export default function RightButton({handleNextClick}:{handleNextClick:()=>void}) {
+ return (
+
+
+
+ )
+}
diff --git a/frontend/components/software/overview/SearchInput.tsx b/frontend/components/software/overview/SearchInput.tsx
index 0b1c9d1d4..42030fd52 100644
--- a/frontend/components/software/overview/SearchInput.tsx
+++ b/frontend/components/software/overview/SearchInput.tsx
@@ -52,7 +52,6 @@ export default function SearchInput({
return (
void
setModal: (modal: boolean) => void
setView: (view:LayoutType)=>void
@@ -39,7 +40,7 @@ export default function SearchSection({
}: SearchSectionProps) {
const smallScreen = useMediaQuery('(max-width:640px)')
return (
-
+
- {/* Items */}
- 12
- 24
- 48
+ {/* load page options from config */}
+ {rowsPerPageOptions.map(item => {item} ) }
diff --git a/frontend/components/software/overview/SoftwareCard.tsx b/frontend/components/software/overview/SoftwareCard.tsx
deleted file mode 100644
index 7f2a23050..000000000
--- a/frontend/components/software/overview/SoftwareCard.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all)
-// SPDX-FileCopyrightText: 2023 dv4all
-//
-// SPDX-License-Identifier: Apache-2.0
-
-import Link from 'next/link'
-import {SoftwareListItem} from '~/types/SoftwareTypes'
-import {getImageUrl} from '~/utils/editImage'
-
-type SoftwareCardProps = {
- item: SoftwareListItem;
- direction?: string
-}
-
-export const SoftwareCard = ({item, direction}:SoftwareCardProps) => {
-
- const visibleNumberOfKeywords: number = 3
- const visibleNumberOfProgLang: number = 3
- const isHorizontal = !!direction
-
- return (
-
-
- {/* Cover image */}
- {
- item.image_id &&
-
- }
-
- {/* Card content */}
-
-
- {item.brand_name}
-
-
- {item.short_statement}
-
-
- {/* keywords */}
-
- {// limits the keywords to 'visibleNumberOfKeywords' per software.
- item.keywords?.slice(0, visibleNumberOfKeywords)
- .map((keyword:string, index: number) => (
- {keyword}
- ))}
-
- { // Show the number of keywords that are not visible.
- (item.keywords?.length > 0)
- && (item.keywords?.length > visibleNumberOfKeywords)
- && (item.keywords?.length - visibleNumberOfKeywords > 0)
- && `+ ${item.keywords?.length - visibleNumberOfKeywords}`
- }
-
-
- {/*
*/}
-
-
- {/* Languages */}
-
- {// limits the keywords to 'visibleNumberOfProgLang' per software.
- item.prog_lang?.slice(0, visibleNumberOfProgLang)
- .map((lang:string, index: number) => (
- {lang}
- ))}
- { // Show the number of keywords that are not visible.
- (item.prog_lang?.length > 0)
- && (item.prog_lang?.length > visibleNumberOfProgLang)
- && (item.prog_lang?.length - visibleNumberOfProgLang > 0)
- && `+ ${item.prog_lang?.length - visibleNumberOfProgLang}`
- }
-
- {/* Metrics */}
-
-
-
-
-
-
{item.contributor_cnt || 0}
-
-
-
-
-
-
-
{item.mention_cnt || 0}
-
-
- {/* TODO Add download counts to the cards */}
- {item?.downloads && item?.downloads > 0 && (
-
- )}
-
-
-
-
-
- )
-}
-// TODO Only show images every 3rd card for testing purposes
-// index % 3 === 0 <-- todo
diff --git a/frontend/components/software/overview/SoftwareFiltersPanel.tsx b/frontend/components/software/overview/SoftwareFiltersPanel.tsx
index 179a36dc8..5cb6682c4 100644
--- a/frontend/components/software/overview/SoftwareFiltersPanel.tsx
+++ b/frontend/components/software/overview/SoftwareFiltersPanel.tsx
@@ -6,7 +6,9 @@
export default function SoftwareFiltersPanel({children}: { children: any }) {
return (
-
+
{children}
)
diff --git a/frontend/components/software/overview/SoftwareGridCard.tsx b/frontend/components/software/overview/SoftwareGridCard.tsx
index 15f613e7e..81a7a641c 100644
--- a/frontend/components/software/overview/SoftwareGridCard.tsx
+++ b/frontend/components/software/overview/SoftwareGridCard.tsx
@@ -8,59 +8,53 @@ import Link from 'next/link'
import {SoftwareListItem} from '~/types/SoftwareTypes'
import {getImageUrl} from '~/utils/editImage'
-import KeywordList from './KeywordList'
-import ProgrammingLanguageList from './ProgrammingLanguageList'
-import SoftwareMetrics from './SoftwareMetrics'
+import KeywordList from '../../cards/KeywordList'
+import ProgrammingLanguageList from '../../cards/ProgrammingLanguageList'
+import SoftwareMetrics from '../../cards/SoftwareMetrics'
+import CardTitleSubtitle from '../../cards/CardTitleSubtitle'
+import ImageWithPlaceholder from '~/components/layout/ImageWithPlaceholder'
type SoftwareCardProps = {
- item: SoftwareListItem;
- direction?: string
+ item: SoftwareListItem
}
-export default function SoftwareGridCard({item, direction}:SoftwareCardProps){
+export default function SoftwareGridCard({item}:SoftwareCardProps){
const visibleNumberOfKeywords: number = 3
const visibleNumberOfProgLang: number = 3
return (
{/* Card content */}
- {/* Cover image - 40% of card height */}
- {
- item.image_id &&
-
-
-
- }
- {/* Card body - 60% of card height */}
-
-
- {item.brand_name}
-
-
-
- {item.short_statement}
-
-
-
- {/* keywords */}
-
-
+
+ {/* Card body - 67% of card height */}
+
+
+
+ {/* keywords */}
+
+
+
- {/*
*/}
-
+
)
}
diff --git a/frontend/components/software/highlights/SoftwareHighlights.tsx b/frontend/components/software/overview/SoftwareHighlights.tsx
similarity index 91%
rename from frontend/components/software/highlights/SoftwareHighlights.tsx
rename to frontend/components/software/overview/SoftwareHighlights.tsx
index 0dd3d634d..0cf43cdbd 100644
--- a/frontend/components/software/highlights/SoftwareHighlights.tsx
+++ b/frontend/components/software/overview/SoftwareHighlights.tsx
@@ -4,7 +4,7 @@
//
// SPDX-License-Identifier: Apache-2.0
-import {HighlightsCarousel} from './HighlightsCarousel'
+import {HighlightsCarousel} from '../highlights/HighlightsCarousel'
import {SoftwareHighlight} from '~/components/admin/software-highlights/apiSoftwareHighlights'
export default function SoftwareHighlights({highlights}: { highlights: SoftwareHighlight[] }) {
diff --git a/frontend/components/software/overview/SoftwareMasonryCard.tsx b/frontend/components/software/overview/SoftwareMasonryCard.tsx
new file mode 100644
index 000000000..d5053dd16
--- /dev/null
+++ b/frontend/components/software/overview/SoftwareMasonryCard.tsx
@@ -0,0 +1,71 @@
+// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all)
+// SPDX-FileCopyrightText: 2023 dv4all
+//
+// SPDX-License-Identifier: Apache-2.0
+
+import Link from 'next/link'
+import {SoftwareListItem} from '~/types/SoftwareTypes'
+import {getImageUrl} from '~/utils/editImage'
+import KeywordList from '~/components/cards/KeywordList'
+import CardTitleSubtitle from '~/components/cards/CardTitleSubtitle'
+import ProgrammingLanguageList from '~/components/cards/ProgrammingLanguageList'
+import SoftwareMetrics from '~/components/cards/SoftwareMetrics'
+
+type SoftwareCardProps = {
+ item: SoftwareListItem
+}
+
+export default function SoftwareMasonryCard({item}:SoftwareCardProps){
+
+ const visibleNumberOfKeywords: number = 3
+ const visibleNumberOfProgLang: number = 3
+
+ return (
+
+
+ {/* Cover image */}
+ {
+ item.image_id &&
+
+ }
+
+ {/* Card content */}
+
+
+
+
+
+
+ {/* Languages */}
+
+ {/* Metrics */}
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/components/software/overview/SoftwareOverviewContent.tsx b/frontend/components/software/overview/SoftwareOverviewContent.tsx
index 85d9c5a2d..8f7f47dd6 100644
--- a/frontend/components/software/overview/SoftwareOverviewContent.tsx
+++ b/frontend/components/software/overview/SoftwareOverviewContent.tsx
@@ -8,6 +8,7 @@ import {LayoutType} from './SearchSection'
import SoftwareOverviewMasonry from './SoftwareOverviewMasonry'
import SoftwareOverviewGrid from './SoftwareOverviewGrid'
import SoftwareOverviewList from './SoftwareOverviewList'
+import NoContent from '~/components/layout/NoContent'
type SoftwareOverviewContentProps = {
layout: LayoutType
@@ -16,6 +17,10 @@ type SoftwareOverviewContentProps = {
export default function SoftwareOverviewContent({layout, software}: SoftwareOverviewContentProps) {
+ if (!software || software.length === 0) {
+ return
+ }
+
if (layout === 'masonry') {
// Masenory grid layout
return (
diff --git a/frontend/components/software/overview/SoftwareOverviewGrid.tsx b/frontend/components/software/overview/SoftwareOverviewGrid.tsx
index 99883e635..2b73e4d9d 100644
--- a/frontend/components/software/overview/SoftwareOverviewGrid.tsx
+++ b/frontend/components/software/overview/SoftwareOverviewGrid.tsx
@@ -11,12 +11,13 @@ import FlexibleGridSection from '~/components/layout/FlexibleGridSection'
export default function SoftwareOverviewGrid({software = []}: { software: SoftwareListItem[] }) {
const grid={
height: '28rem',
- minWidth: '19rem',
+ minWidth: '18rem',
maxWidth: '1fr'
}
return (
diff --git a/frontend/components/software/overview/SoftwareOverviewList.tsx b/frontend/components/software/overview/SoftwareOverviewList.tsx
index 5dbb6f910..57a639c92 100644
--- a/frontend/components/software/overview/SoftwareOverviewList.tsx
+++ b/frontend/components/software/overview/SoftwareOverviewList.tsx
@@ -12,61 +12,68 @@ import {getImageUrl} from '~/utils/editImage'
import ContributorIcon from '~/components/icons/ContributorIcon'
import MentionIcon from '~/components/icons/MentionIcon'
import DownloadsIcon from '~/components/icons/DownloadsIcon'
+import ImageWithPlaceholder from '~/components/layout/ImageWithPlaceholder'
export default function SoftwareOverviewList({software = []}: { software: SoftwareListItem[] }) {
return (
-
-
- {software.map(item => (
-
-
- {item.image_id &&
- // eslint-disable-next-line @next/next/no-img-element
-
}
-
-
-
-
+
+ {software.map(item => (
+
+
+ {item.image_id ?
+
+ :
+
+ }
+
+
+
{item.brand_name}
-
-
- {item.short_statement}
-
+
+ {item.short_statement}
+
+
- {/* Indicators */}
-
-
-
- {item.contributor_cnt || 0}
-
-
-
- {item.mention_cnt || 0}
-
+ {/* Indicators */}
+
+
+
+ {item.contributor_cnt || 0}
+
+
+
+ {item.mention_cnt || 0}
+
- {/* TODO Add download counts to the cards */}
- {(item?.downloads && item?.downloads > 0) &&
-
-
- 34K
-
- }
+ {/* TODO Add download counts to the cards */}
+ {(item?.downloads && item?.downloads > 0) &&
+
+
+ 34K
+ }
-
- )
- )}
-
+
+
+ )
+ )}
)
}
diff --git a/frontend/components/software/overview/SoftwareOverviewMasonry.tsx b/frontend/components/software/overview/SoftwareOverviewMasonry.tsx
index 4fef85163..a4539e36e 100644
--- a/frontend/components/software/overview/SoftwareOverviewMasonry.tsx
+++ b/frontend/components/software/overview/SoftwareOverviewMasonry.tsx
@@ -5,14 +5,16 @@
// SPDX-License-Identifier: Apache-2.0
import {SoftwareListItem} from '~/types/SoftwareTypes'
-import {SoftwareCard} from './SoftwareCard'
+import SoftwareMasonryCard from './SoftwareMasonryCard'
export default function SoftwareOverviewMasonry({software=[]}: { software:SoftwareListItem[]}) {
return (
-
+
{software.map((item, index) => (
-
+
))}
diff --git a/frontend/components/software/overview/filters/FilterHeader.tsx b/frontend/components/software/overview/filters/FilterHeader.tsx
index c31d2d4e8..e34ecfa61 100644
--- a/frontend/components/software/overview/filters/FilterHeader.tsx
+++ b/frontend/components/software/overview/filters/FilterHeader.tsx
@@ -18,7 +18,7 @@ export default function FilterHeader({filterCnt,resetFilters}:FilterHeaderProps)
className="rounded-full bg-gray-100 h-8 w-8 flex items-center justify-center font-semibold">
{filterCnt}
- Filters
+ {filterCnt===1 ? 'Filter' : 'Filters'}
,
+ label: string,
+ count: number
+}
+
+export default function FilterOption({props,label,count}:FilterOptionProps) {
+ return (
+
+
+ {label}
+
+
+ ({count})
+
+
+ )
+}
diff --git a/frontend/components/software/overview/filters/FilterTitle.tsx b/frontend/components/software/overview/filters/FilterTitle.tsx
new file mode 100644
index 000000000..72f9e5e96
--- /dev/null
+++ b/frontend/components/software/overview/filters/FilterTitle.tsx
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2023 dv4all
+//
+// SPDX-License-Identifier: Apache-2.0
+
+export default function FilterTitle({title,count}:{title:string,count:number}) {
+ return (
+
+ )
+}
diff --git a/frontend/components/software/overview/filters/KeywordsFilter.tsx b/frontend/components/software/overview/filters/KeywordsFilter.tsx
index b98c1bfa8..fd4eb867e 100644
--- a/frontend/components/software/overview/filters/KeywordsFilter.tsx
+++ b/frontend/components/software/overview/filters/KeywordsFilter.tsx
@@ -8,6 +8,8 @@ import Autocomplete from '@mui/material/Autocomplete'
import TextField from '@mui/material/TextField'
import {KeywordFilterOption} from './softwareFiltersApi'
+import FilterTitle from './FilterTitle'
+import FilterOption from './FilterOption'
type KeywordsFilterProps = {
keywords: string[],
@@ -38,10 +40,10 @@ export default function KeywordsFilter({keywords,keywordsList,handleQueryChange}
return (
-
-
Keywords
-
{keywordsList.length}
-
+
(
-
- {
- option.keyword
- }
- ({
- option.keyword_cnt
- })
-
-
+
)}
renderInput={(params) => (
diff --git a/frontend/components/software/overview/filters/LicensesFilter.tsx b/frontend/components/software/overview/filters/LicensesFilter.tsx
index 16e67b363..089336515 100644
--- a/frontend/components/software/overview/filters/LicensesFilter.tsx
+++ b/frontend/components/software/overview/filters/LicensesFilter.tsx
@@ -8,6 +8,8 @@ import {useEffect, useState} from 'react'
import Autocomplete from '@mui/material/Autocomplete'
import TextField from '@mui/material/TextField'
import {LicensesFilterOption} from './softwareFiltersApi'
+import FilterTitle from './FilterTitle'
+import FilterOption from './FilterOption'
type LicensesFilterProps = {
@@ -32,10 +34,10 @@ export default function LicensesFilter({licenses,licensesList,handleQueryChange}
return (
-
-
Licenses
-
{licensesList.length}
-
+
(
-
- {option.license}
- ({option.license_cnt})
-
+
)}
renderInput={(params) => (
diff --git a/frontend/components/software/overview/filters/OrderBy.tsx b/frontend/components/software/overview/filters/OrderBy.tsx
index df5260fe6..e72a629fe 100644
--- a/frontend/components/software/overview/filters/OrderBy.tsx
+++ b/frontend/components/software/overview/filters/OrderBy.tsx
@@ -8,6 +8,12 @@ import InputLabel from '@mui/material/InputLabel'
import MenuItem from '@mui/material/MenuItem'
import Select from '@mui/material/Select'
+export const softwareOrderOptions = [
+ {key: 'contributor_cnt', label: 'Contributors', direction:'desc.nullslast'},
+ {key: 'mention_cnt', label: 'Mentions', direction:'desc.nullslast'},
+ {key: 'brand_name', label: 'Name', direction:'asc'},
+]
+
type OrderByProps = {
orderBy: string
handleQueryChange: (key: string, value: string | string[]) => void
@@ -15,20 +21,24 @@ type OrderByProps = {
export default function OrderBy({orderBy,handleQueryChange}:OrderByProps) {
return (
-
- Order by
+
+ Order by
{
- // setOrderBy(e.target.value)
handleQueryChange('order', e.target.value)
}}
>
- Contributions
- Mentions
+ {
+ softwareOrderOptions.map(option => {option.label} )
+ }
)
diff --git a/frontend/components/software/overview/filters/ProgrammingLanguagesFilter.tsx b/frontend/components/software/overview/filters/ProgrammingLanguagesFilter.tsx
index d9f6a93ec..a8eb3f695 100644
--- a/frontend/components/software/overview/filters/ProgrammingLanguagesFilter.tsx
+++ b/frontend/components/software/overview/filters/ProgrammingLanguagesFilter.tsx
@@ -7,8 +7,9 @@ import {useEffect, useState} from 'react'
import Autocomplete from '@mui/material/Autocomplete'
import TextField from '@mui/material/TextField'
-import {ProgrammingLanguage} from '../../filter/softwareFilterApi'
import {LanguagesFilterOption} from './softwareFiltersApi'
+import FilterTitle from './FilterTitle'
+import FilterOption from './FilterOption'
type ProgrammingLanguagesFilterProps = {
prog_lang: string[],
@@ -32,10 +33,10 @@ export default function ProgrammingLanguagesFilter({prog_lang,languagesList,hand
return (
-
-
Program languages
-
{languagesList.length}
-
+
(
-
- {
- option.prog_language
- }
- ({
- option.prog_language_cnt
- })
-
-
+
)}
renderInput={(params) => (
diff --git a/frontend/components/software/overview/filters/index.tsx b/frontend/components/software/overview/filters/index.tsx
index 3e681df1e..d7489ea5b 100644
--- a/frontend/components/software/overview/filters/index.tsx
+++ b/frontend/components/software/overview/filters/index.tsx
@@ -4,9 +4,6 @@
//
// SPDX-License-Identifier: Apache-2.0
-// TODO! examine what can be removed
-import {Keyword} from '~/components/keyword/FindKeyword'
-import {ProgrammingLanguage} from '../../filter/softwareFilterApi'
import FilterHeader from './FilterHeader'
import OrderBy from './OrderBy'
import KeywordsFilter from './KeywordsFilter'
diff --git a/frontend/components/software/overview/useSoftwareParams.ts b/frontend/components/software/overview/useSoftwareParams.ts
index 1ddebf7f3..8a5719322 100644
--- a/frontend/components/software/overview/useSoftwareParams.ts
+++ b/frontend/components/software/overview/useSoftwareParams.ts
@@ -28,7 +28,7 @@ export default function useSoftwareParams() {
params['rows'] = getDocumentCookie('rsd_page_rows',12)
}
// construct url with all query params
- const url = buildFilterUrl(params, 'highlights')
+ const url = buildFilterUrl(params, 'software')
if (key === 'page') {
// when changin page we scroll to top
router.push(url, url, {scroll: true})
diff --git a/frontend/components/softwarePage/FeaturedSoftwareCarousel.tsx b/frontend/components/softwarePage/FeaturedSoftwareCarousel.tsx
deleted file mode 100644
index 74f99b4dc..000000000
--- a/frontend/components/softwarePage/FeaturedSoftwareCarousel.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2023 dv4all
-//
-// SPDX-License-Identifier: Apache-2.0
-
-import {UIEventHandler, useRef, useState} from 'react'
-import {SoftwareCard} from './SoftwareCard'
-
-export const FeaturedSoftwareCarousel = ({cards}: {cards:any}) => {
-
- const canrdMovement: number = 680 // card size + margin
- // const canrdMovementVertical: number = 320 // card size + margin
-
- // Keep track of the current scroll position of the carousel.
- const [scrollPosition, setScrollPosition] = useState(0)
- const carousel = useRef(null)
-
- // Event handlers for the next and previous buttons.
- const handleNextClick = () => {
- // move the scroll to the left
- if (carousel.current) {
- carousel.current.scrollLeft -= canrdMovement
- }
- }
-
- const handlePrevClick = () => {
- if (carousel.current) {
- carousel.current.scrollLeft += canrdMovement
- }
- }
-
-// TODO
- const handleScroll:UIEventHandler = (event:any) => {
- // update the scroll position state variable whenever the user scrolls
- setScrollPosition(event.target.scrollLeft)
- }
-
- return (
-
- {/* Left Button */}
- {scrollPosition > 0 &&
-
-
-
- }
-
- {/* Right Button */}
-
-
-
-
-
- {/* Carousel */}
-
- {/* TODO software card type */}
- {cards.length > 0 && cards.map((card: any, index: number) => (
-
-
-
- ))
- }
-
-
- )
-}
diff --git a/frontend/components/softwarePage/SearchInput.tsx b/frontend/components/softwarePage/SearchInput.tsx
deleted file mode 100644
index c2156e70f..000000000
--- a/frontend/components/softwarePage/SearchInput.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2022 - 2023 dv4all
-//
-// SPDX-License-Identifier: Apache-2.0
-
-import {useState, useEffect} from 'react'
-import {useDebounce} from '~/utils/useDebounce'
-import TextField from '@mui/material/TextField'
-
-type SearchInputProps = {
- placeholder: string,
- onSearch: Function,
- delay?: number,
- defaultValue?: string,
-}
-
-export default function SearchInput({
- placeholder,
- onSearch,
- delay = 400,
- defaultValue = ''
-}: SearchInputProps) {
- const [state, setState] = useState({
- value: defaultValue ?? '',
- wait: true
- })
- const searchFor = useDebounce(state.value, delay)
-
- useEffect(() => {
- if ((searchFor !== '' && defaultValue === '') || defaultValue !== '') {
- setState({value: defaultValue, wait: true})
- }
- }, [searchFor, defaultValue])
-
- useEffect(() => {
- let abort = false
- const {wait, value} = state
- if (!wait && value === searchFor) {
- if (abort) return
- setState({
- wait: true,
- value
- })
- onSearch(searchFor)
- }
- return () => {
- abort = true
- }
- }, [state, searchFor, onSearch])
-
- return (
- setState({value: target.value, wait: false})}
- />
- )
-}
diff --git a/frontend/components/softwarePage/SoftwareCard.tsx b/frontend/components/softwarePage/SoftwareCard.tsx
deleted file mode 100644
index 0b35ec1bf..000000000
--- a/frontend/components/softwarePage/SoftwareCard.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2023 dv4all
-//
-// SPDX-License-Identifier: Apache-2.0
-
-/* eslint-disable @next/next/no-img-element */
-import Link from 'next/link'
-
-export const SoftwareCard = ({item, direction, index}: {
- item: any; direction?: string; index: number;
-}) => {
-
- const visibleNumberOfKeywords: number = 3
- const visibleNumberOfProgLang: number = 3
-
- const isHorizontal = !!direction
- return (
-
-
- {/* Cover image */}
-
-
- {/* Card content */}
-
-
-
- {item.brand_name}
-
-
- {item.short_statement}
-
- {/* keywords */}
-
-
- {// limits the keywords to 'visibleNumberOfKeywords' per software.
- item.keywords?.slice(0, visibleNumberOfKeywords)
- .map((keyword:string, index: number) => (
- {keyword}
- ))}
-
- { // Show the number of keywords that are not visible.
- (item.keywords?.length > 0)
- && (item.keywords?.length > visibleNumberOfKeywords)
- && (item.keywords?.length - visibleNumberOfKeywords > 0)
- && `+ ${item.keywords?.length - visibleNumberOfKeywords}`
- }
-
-
-
-
-
- {/* Languages */}
-
- {// limits the keywords to 'visibleNumberOfProgLang' per software.
- item.prog_lang?.slice(0, visibleNumberOfProgLang)
- .map((lang:string, index: number) => (
- {lang}
- ))}
- { // Show the number of keywords that are not visible.
- (item.prog_lang?.length > 0)
- && (item.prog_lang?.length > visibleNumberOfProgLang)
- && (item.prog_lang?.length - visibleNumberOfProgLang > 0)
- && `+ ${item.prog_lang?.length - visibleNumberOfProgLang}`
- }
-
- {/* Metrics */}
-
-
-
-
-
-
{item.contributor_cnt || 0}
-
-
-
-
-
-
-
{item.mention_cnt || 0}
-
-
- {/* TODO Add download counts to the cards */}
- {item.downloads > 0 && (
-
- )}
-
-
-
-
-
- )
-}
-// TODO Only show images every 3rd card for testing purposes
-// index % 3 === 0 <-- todo
diff --git a/frontend/components/softwarePage/SoftwareFilterPanel.tsx b/frontend/components/softwarePage/SoftwareFilterPanel.tsx
deleted file mode 100644
index 00e8fd451..000000000
--- a/frontend/components/softwarePage/SoftwareFilterPanel.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2023 dv4all
-//
-// SPDX-License-Identifier: Apache-2.0
-
-import {ForwardedRef} from 'react'
-import Button from '@mui/material/Button'
-import FormControl from '@mui/material/FormControl'
-import InputLabel from '@mui/material/InputLabel'
-import Select from '@mui/material/Select'
-import MenuItem from '@mui/material/MenuItem'
-import Autocomplete from '@mui/material/Autocomplete'
-import TextField from '@mui/material/TextField'
-import useSoftwareFilterPanel from '~/components/softwarePage/useSoftwarefilterPanel'
-
-// Ref needed to emebd a custom component inside a MUI modal:
-// https://mui.com/material-ui/guides/composition/#caveat-with-refs
-type Ref = {
- ref?: ForwardedRef
-}
-
-export default function SoftwareFilterPanel({ref}: Ref) {
- const {
- keywords,
- keywordsList,
- languages,
- languagesList,
- licenses,
- licensesList,
- handleQueryChange,
- orderBy, setOrderBy,
- getFilterCount,
- resetFilters
- } = useSoftwareFilterPanel()
-
- // @ts-ignore
- return
-
-
-
- {getFilterCount()}
-
- Filters
-
-
-
- Clear
-
-
-
- {/* Order by */}
-
- Order by
- {
- setOrderBy(e.target.value)
- handleQueryChange('order', e.target.value)
- }}
- >
- Contributions
- Mentions
-
-
-
- {/* Keywords */}
-
-
-
Keywords
-
{keywordsList.length}
-
-
(option.keyword)}
- isOptionEqualToValue={(option, value) => {
- return option.keyword === value.keyword
- }}
- defaultValue={[]}
- filterSelectedOptions
- renderOption={(props, option) => (
-
- {
- option.keyword
- }
- ({
- option.cnt
- })
-
-
- )}
- renderInput={(params) => (
-
- )}
- onChange={(event, newValue) => {
- // extract values into string[] for url query
- const queryFilter = newValue.map(item => item.keyword)
- handleQueryChange('keywords', queryFilter)
- }}
- />
-
-
- {/* Programme Languages */}
-
-
-
Program languages
-
{languagesList.length}
-
-
option.prog_lang}
- isOptionEqualToValue={(option, value) => {
- return option.prog_lang === value.prog_lang
- }}
- defaultValue={[]}
- filterSelectedOptions
- renderOption={(props, option) => (
-
- {
- option.prog_lang
- }
- ({
- option.cnt
- })
-
-
- )}
- renderInput={(params) => (
-
- )}
- onChange={(event, newValue) => {
- // extract values into string[] for url query
- const queryFilter = newValue.map(item => item.prog_lang)
- // update query url
- handleQueryChange('prog_lang', queryFilter)
- }}
- />
-
-
- {/* Licenses */}
-
-
-
Licenses
-
{licensesList.length}
-
-
option.license}
- isOptionEqualToValue={(option, value) => {
- return option.license === value.license
- }}
- defaultValue={[]}
- filterSelectedOptions
- renderOption={(props, option) => (
-
- {option.license}
- ({option.cnt})
-
- )}
- renderInput={(params) => (
-
- )}
- onChange={(event, newValue) => {
- // extract values into string[] for url query
- const queryFilter = newValue.map(item => item.license)
- // update query url
- handleQueryChange('licenses', queryFilter)
- }}
- />
-
-
-}
diff --git a/frontend/components/softwarePage/softwarePagePanel.d.ts b/frontend/components/softwarePage/softwarePagePanel.d.ts
deleted file mode 100644
index 395af70e6..000000000
--- a/frontend/components/softwarePage/softwarePagePanel.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2023 dv4all
-//
-// SPDX-License-Identifier: Apache-2.0
-
-export type License = {
- license: string;
- cnt: number;
-}
diff --git a/frontend/components/softwarePage/useSoftwarefilterPanel.ts b/frontend/components/softwarePage/useSoftwarefilterPanel.ts
deleted file mode 100644
index 94e6ebcd7..000000000
--- a/frontend/components/softwarePage/useSoftwarefilterPanel.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all)
-// SPDX-FileCopyrightText: 2023 dv4all
-//
-// SPDX-License-Identifier: Apache-2.0
-
-import {useRouter} from 'next/router'
-import {getBaseUrl} from '~/utils/fetchHelpers'
-import {useEffect, useState} from 'react'
-import {ProgrammingLanguage} from '~/components/software/filter/softwareFilterApi'
-import {SoftwareListItem} from '~/types/SoftwareTypes'
-import {ssrSoftwareParams} from '~/utils/extractQueryParam'
-import {buildFilterUrl, softwareListUrl} from '~/utils/postgrestUrl'
-import {getSoftwareList} from '~/utils/getSoftware'
-import {Keyword} from '../keyword/FindKeyword'
-import {License} from '~/components/softwarePage/softwarePagePanel'
-
-export default function useSoftwarefilterPanel() {
- const router = useRouter()
- const baseUrl = getBaseUrl()
-
- const [orderBy, setOrderBy] = useState('')
- const [search, setSearch] = useState('')
-
- // keyword list is an array of objects or a string
- const [keywordsList, setKeywordsList] = useState([])
- const [keywords, setKeywords] = useState([])
-
- const [languages, setLanguages] = useState([])
- const [languagesList, setLanguagesList] = useState([])
-
- const [licenses, setLicenses] = useState([])
- const [licensesList, setLicensesList] = useState([])
-
- const [software, setSoftware] = useState<{ count: number, items: SoftwareListItem[] }>({
- count: 0,
- items: []
- })
-
- useEffect(() => {
- if (baseUrl) {
- // fetch keywords list
- fetch(`${baseUrl}/rpc/keyword_count_for_software?keyword=ilike.**&cnt=gt.0&order=cnt.desc.nullslast,keyword.asc`)
- .then((response) => response.json())
- .then((data) => setKeywordsList(data))
-
- // fetch programme languages list
- fetch(`${baseUrl}/rpc/prog_lang_cnt_for_software?prog_lang=ilike.**&cnt=gt.0&order=cnt.desc.nullslast,prog_lang.asc`)
- .then((response) => response.json())
- .then((data) => setLanguagesList(data))
-
- // fetch licenses list
- fetch(`${baseUrl}/rpc/license_cnt_for_software`)
- .then((response) => response.json())
- .then((data) => {
- setLicensesList(data)
- })
- }
- }, [baseUrl])
-
- useEffect(() => {
- if (search !== '') {
- const {search:searchInput} = ssrSoftwareParams(router.query)
- if (searchInput && searchInput !== '') {
- setSearch(searchInput)
- } else {
- setSearch('')
- }
- }
- }, [router.query,search])
-
- useEffect(() => {
- if (orderBy !== '') {
- const {order} = ssrSoftwareParams(router.query)
- if (order && order !== '') {
- setOrderBy(order)
- } else {
- setOrderBy('')
- }
- }
- }, [router.query, orderBy])
-
- useEffect(() => {
- if (keywordsList.length > 0) {
- const {keywords} = ssrSoftwareParams(router.query)
- if (keywords && keywords.length > 0) {
- const selectedKeywords: Keyword[] = keywordsList.filter(option => {
- return keywords.includes(option.keyword)
- })
- setKeywords(selectedKeywords)
- } else {
- setKeywords([])
- }
- }
- }, [keywordsList, router.query])
-
- useEffect(() => {
- if (languagesList.length > 0) {
- const {prog_lang} = ssrSoftwareParams(router.query)
- if (prog_lang && prog_lang.length > 0) {
- const selectedProgLang: ProgrammingLanguage[] = languagesList.filter(option => {
- return prog_lang.includes(option.prog_lang)
- })
- setLanguages(selectedProgLang)
- } else {
- setLanguages([])
- }
- }
- }, [languagesList, router.query])
-
- useEffect(() => {
- if (licensesList.length > 0) {
- const {licenses} = ssrSoftwareParams(router.query)
- if (licenses && licenses.length > 0) {
- const selected: License[] = licensesList.filter(option => {
- return licenses.includes(option.license)
- })
- setLicenses(selected)
- } else {
- setLicenses([])
- }
- }
- }, [licensesList, router.query])
-
- useEffect(() => {
- let orderBy
- // extract params from page-query
- const {search, keywords, prog_lang, licenses, order, page} = ssrSoftwareParams(router.query)
-
- // update components based on query params
- if (order) {
- setOrderBy(order)
- orderBy = `${orderBy}.desc.nullslast`
- }
- if (search) {
- setSearch(search)
- }
-
- //build api url
- const url = softwareListUrl({
- baseUrl,
- search,
- keywords,
- licenses,
- order: orderBy,
- prog_lang,
- limit: 24,
- offset: 24 * (page ?? 0)
- })
-
- // get software list from api
- getSoftwareList({url})
- .then(resp => {
- setSoftware({
- count: resp.count ?? 0,
- items: resp.data ?? []
- })
- })
-
- }, [router.query, baseUrl])
-
-
- function handleQueryChange(key: string, value: string | string[]) {
- const url = buildFilterUrl({
- // take existing params from url (query)
- ...ssrSoftwareParams(router.query),
- [key]: value,
- // start from first page
- page: 0,
- // use 24 items
- rows: 24
- }, 'highlights')
-
- // update page url
- router.push(url)
- }
-
- function getFilterCount() {
- let count = 0
- if (orderBy !== '') count++
- if (keywords.length > 0) count++
- if (languages.length > 0) count++
- if (licenses.length > 0) count++
- if (search !== '') count++
- return count
- }
-
- function resetFilters() {
- // return pathname without filters/query
- router.replace(router.pathname, undefined, {shallow: true})
- }
-
- return {
- orderBy, setOrderBy,
- keywords, keywordsList,
- languages, languagesList,
- licenses, licensesList,
- software, setSoftware,
- search, setSearch,
- handleQueryChange,
- getFilterCount,
- resetFilters
- }
-}
-
diff --git a/frontend/next.config.js b/frontend/next.config.js
index 802426667..3104fbd95 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -1,5 +1,5 @@
-// SPDX-FileCopyrightText: 2021 - 2022 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2021 - 2022 dv4all
+// SPDX-FileCopyrightText: 2021 - 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2021 - 2023 dv4all
// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2022 Jesús García Gonzalez (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2022 Netherlands eScience Center
@@ -15,6 +15,8 @@ module.exports = {
// create standalone output to use in docker image
// and achieve minimal image size (see Dockerfile)
output: 'standalone',
+ // enable source maps in production?
+ productionBrowserSourceMaps: true,
// disable strict mode if you want react to render compent once
// see for more info https://nextjs.org/docs/api-reference/next.config.js/react-strict-mode
reactStrictMode: false,
diff --git a/frontend/pages/highlights/index.tsx b/frontend/pages/highlights/index.tsx
deleted file mode 100644
index 68137f479..000000000
--- a/frontend/pages/highlights/index.tsx
+++ /dev/null
@@ -1,289 +0,0 @@
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all)
-// SPDX-FileCopyrightText: 2023 dv4all
-//
-// SPDX-License-Identifier: Apache-2.0
-
-import {useEffect, useState} from 'react'
-import {GetServerSidePropsContext} from 'next/types'
-import useMediaQuery from '@mui/material/useMediaQuery'
-import Pagination from '@mui/material/Pagination'
-
-import {app} from '~/config/app'
-import {getBaseUrl} from '~/utils/fetchHelpers'
-import {softwareListUrl} from '~/utils/postgrestUrl'
-import {getSoftwareList} from '~/utils/getSoftware'
-import {ssrSoftwareParams} from '~/utils/extractQueryParam'
-import {SoftwareListItem} from '~/types/SoftwareTypes'
-import MainContent from '~/components/layout/MainContent'
-import AppHeader from '~/components/AppHeader'
-import AppFooter from '~/components/AppFooter'
-import SoftwareFiltersPanel from '~/components/software/overview/SoftwareFiltersPanel'
-import SoftwareHighlights from '~/components/software/highlights/SoftwareHighlights'
-import OverviewPageBackground from '~/components/software/overview/PageBackground'
-import SearchSection, {LayoutType} from '~/components/software/overview/SearchSection'
-import useSoftwareParams from '~/components/software/overview/useSoftwareParams'
-import SoftwareOverviewContent from '~/components/software/overview/SoftwareOverviewContent'
-import SoftwareFilters from '~/components/software/overview/filters/index'
-import {
- KeywordFilterOption, LanguagesFilterOption, LicensesFilterOption,
- softwareKeywordsFilter, softwareLanguagesFilter,
- softwareLicesesFilter
-} from '~/components/software/overview/filters/softwareFiltersApi'
-import FilterModal from '~/components/software/overview/filters/FilterModal'
-import PageMeta from '~/components/seo/PageMeta'
-import CanonicalUrl from '~/components/seo/CanonicalUrl'
-import {getUserSettings, setDocumentCookie} from '~/components/software/overview/userSettings'
-import {SoftwareHighlight, getSoftwareHighlights} from '~/components/admin/software-highlights/apiSoftwareHighlights'
-
-
-type SoftwareHighlightsPageProps = {
- search?: string
- keywords?: string[],
- keywordsList: KeywordFilterOption[],
- prog_lang?: string[],
- languagesList: LanguagesFilterOption[],
- licenses?: string[],
- licensesList: LicensesFilterOption[],
- order: string,
- page: number,
- rows: number,
- count: number,
- layout: LayoutType,
- software: SoftwareListItem[],
- highlights: SoftwareHighlight[]
-}
-
-const pageTitle = `Software | ${app.title}`
-const pageDesc = 'The list of research software registerd in the Research Software Directory.'
-
-export default function SoftwareHighlightsPage({
- search, keywords,
- prog_lang, licenses,
- order, page, rows,
- count, layout,
- keywordsList, languagesList,
- licensesList, software, highlights
-}: SoftwareHighlightsPageProps) {
- const [view, setView] = useState('masonry')
- const smallScreen = useMediaQuery('(max-width:640px)')
- const {handleQueryChange, resetFilters} = useSoftwareParams()
-
- const [modal,setModal] = useState(false)
- const numPages = Math.ceil(count / rows)
- const filterCnt = getFilterCount()
-
- // console.group('SoftwareHighlightsPage')
- // console.log('search...', search)
- // console.log('keywords...', keywords)
- // console.log('prog_lang...', prog_lang)
- // console.log('licenses...', licenses)
- // console.log('order...', order)
- // console.log('layout...', layout)
- // console.log('view...', view)
- // console.log('software...', software)
- // console.log('highlights...', highlights)
- // console.groupEnd()
-
- // Update view state based on layout value from cookie
- useEffect(() => {
- if (layout) {
- setView(layout)
- }
- },[layout])
-
- function getFilterCount() {
- let count = 0
- if (order) count++
- if (keywords) count++
- if (prog_lang) count++
- if (licenses) count++
- if (search) count++
- return count
- }
-
- function setLayout(view: LayoutType) {
- // update local view
- setView(view)
- // save to cookie
- setDocumentCookie(view,'rsd_page_layout')
- }
-
- return (
- <>
- {/* Page Head meta tags */}
-
- {/* canonical url meta tag */}
-
-
- {/* App header */}
-
- {/* Software Highlights Carousel */}
-
- {/* Main page body */}
-
- {/* Page title */}
-
- All software
-
- {/* Page grid with 2 sections: left filter panel and main content */}
-
- {/* Filters panel large screen */}
- {smallScreen===false &&
-
-
-
- }
- {/* Search & main content section */}
-
-
- {/* Software content: cards or list */}
-
- {/* Pagination */}
-
- {numPages > 1 &&
-
{
- handleQueryChange('page',page.toString())
- }}
- />
- }
-
-
-
-
-
-
- {/* filter for mobile */}
- {
- smallScreen===true &&
-
- }
- >
- )
-}
-
-// fetching data server side
-// see documentation https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering
-export async function getServerSideProps(context: GetServerSidePropsContext) {
- let orderBy, offset=0
- // extract params from page-query
- const {search, keywords, prog_lang, licenses, order, rows, page} = ssrSoftwareParams(context.query)
- // extract user settings from cookie
- const {rsd_page_layout, rsd_page_rows} = getUserSettings(context.req)
- // default rows values comes from user settings
- let page_rows = rsd_page_rows
-
- if (order) {
- orderBy=`${order}.desc.nullslast`
- }
- // if rows && page are provided as query params
- if (rows && page) {
- offset = rows * (page - 1)
- // use rows provided as param
- page_rows = rows
- }
- // construct postgREST api url with query params
- const url = softwareListUrl({
- baseUrl: getBaseUrl(),
- search,
- keywords,
- licenses,
- prog_lang,
- order: orderBy,
- limit: page_rows,
- offset
- })
-
- // console.log('software...url...', url)
- // console.log('search...', search)
- // console.log('page_rows...', page_rows)
-
- // get software items AND filter options
- const [
- software,
- keywordsList,
- languagesList,
- licensesList,
- {highlights}
- ] = await Promise.all([
- getSoftwareList({url}),
- softwareKeywordsFilter({search, keywords, prog_lang, licenses}),
- softwareLanguagesFilter({search, keywords, prog_lang, licenses}),
- softwareLicesesFilter({search, keywords, prog_lang, licenses}),
- getSoftwareHighlights({
- page: 0,
- // get max. 20 items
- rows: 20,
- orderBy: 'position'
- })
- ])
-
- // is passed as props to page
- // see params of page function
- return {
- props: {
- search,
- keywords,
- keywordsList,
- prog_lang,
- languagesList,
- licenses,
- licensesList,
- page,
- order,
- rows: page_rows,
- layout: rsd_page_layout,
- count: software.count,
- software: software.data,
- highlights
- },
- }
-}
diff --git a/frontend/pages/software/index.tsx b/frontend/pages/software/index.tsx
index 52e310587..660d55602 100644
--- a/frontend/pages/software/index.tsx
+++ b/frontend/pages/software/index.tsx
@@ -1,214 +1,301 @@
-// SPDX-FileCopyrightText: 2021 - 2023 Dusan Mijatovic (dv4all)
-// SPDX-FileCopyrightText: 2021 - 2023 dv4all
-// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all)
+// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
+// SPDX-FileCopyrightText: 2023 dv4all
//
// SPDX-License-Identifier: Apache-2.0
-import {MouseEvent, ChangeEvent} from 'react'
-import {useRouter} from 'next/router'
+import {useEffect, useState} from 'react'
import {GetServerSidePropsContext} from 'next/types'
-import TablePagination from '@mui/material/TablePagination'
+import useMediaQuery from '@mui/material/useMediaQuery'
import Pagination from '@mui/material/Pagination'
import {app} from '~/config/app'
-import DefaultLayout from '~/components/layout/DefaultLayout'
-import PageTitle from '~/components/layout/PageTitle'
-import Searchbox from '~/components/form/Searchbox'
-import SoftwareGrid from '~/components/software/SoftwareGrid'
-import {SoftwareListItem} from '~/types/SoftwareTypes'
-import {rowsPerPageOptions} from '~/config/pagination'
+import {getBaseUrl} from '~/utils/fetchHelpers'
+import {softwareListUrl} from '~/utils/postgrestUrl'
import {getSoftwareList} from '~/utils/getSoftware'
import {ssrSoftwareParams} from '~/utils/extractQueryParam'
-import {softwareListUrl,ssrSoftwareUrl} from '~/utils/postgrestUrl'
-import {getBaseUrl} from '~/utils/fetchHelpers'
-import SoftwareFilter from '~/components/software/filter'
-import {useAdvicedDimensions} from '~/components/layout/FlexibleGridSection'
+import {SoftwareListItem} from '~/types/SoftwareTypes'
+import MainContent from '~/components/layout/MainContent'
+import AppHeader from '~/components/AppHeader'
+import AppFooter from '~/components/AppFooter'
import PageMeta from '~/components/seo/PageMeta'
import CanonicalUrl from '~/components/seo/CanonicalUrl'
-import {getUserSettings} from '~/components/software/overview/userSettings'
+import {
+ SoftwareHighlight,
+ getSoftwareHighlights
+} from '~/components/admin/software-highlights/apiSoftwareHighlights'
+import SoftwareFiltersPanel from '~/components/software/overview/SoftwareFiltersPanel'
+import SoftwareHighlights from '~/components/software/overview/SoftwareHighlights'
+import OverviewPageBackground from '~/components/software/overview/PageBackground'
+import SearchSection, {LayoutType} from '~/components/software/overview/SearchSection'
+import useSoftwareParams from '~/components/software/overview/useSoftwareParams'
+import SoftwareOverviewContent from '~/components/software/overview/SoftwareOverviewContent'
+import SoftwareFilters from '~/components/software/overview/filters/index'
+import {
+ KeywordFilterOption, LanguagesFilterOption, LicensesFilterOption,
+ softwareKeywordsFilter, softwareLanguagesFilter,
+ softwareLicesesFilter
+} from '~/components/software/overview/filters/softwareFiltersApi'
+import FilterModal from '~/components/software/overview/filters/FilterModal'
+import {getUserSettings, setDocumentCookie} from '~/components/software/overview/userSettings'
+import {softwareOrderOptions} from '~/components/software/overview/filters/OrderBy'
-type SoftwareIndexPageProps = {
- count: number,
+type SoftwareOverviewProps = {
+ search?: string | null
+ keywords?: string[] | null,
+ keywordsList: KeywordFilterOption[],
+ prog_lang?: string[] | null,
+ languagesList: LanguagesFilterOption[],
+ licenses?: string[] | null,
+ licensesList: LicensesFilterOption[],
+ order?: string | null,
page: number,
rows: number,
- keywords?: string[],
- prog_lang?: string[],
+ count: number,
+ layout: LayoutType,
software: SoftwareListItem[],
- search?: string,
+ highlights: SoftwareHighlight[]
}
const pageTitle = `Software | ${app.title}`
const pageDesc = 'The list of research software registerd in the Research Software Directory.'
-export default function SoftwareIndexPage(
- {software=[], count, page, rows, keywords, prog_lang, search}: SoftwareIndexPageProps
-) {
- // use next router (hook is only for browser)
- const router = useRouter()
- const {itemHeight, minWidth, maxWidth} = useAdvicedDimensions('software')
+export default function SoftwareOverviewPage({
+ search, keywords,
+ prog_lang, licenses,
+ order, page, rows,
+ count, layout,
+ keywordsList, languagesList,
+ licensesList, software, highlights
+}: SoftwareOverviewProps) {
+ const [view, setView] = useState('masonry')
+ const smallScreen = useMediaQuery('(max-width:640px)')
+ const {handleQueryChange, resetFilters} = useSoftwareParams()
- // console.group('SoftwareIndexPage')
- // console.log('query...', router.query)
- // console.groupEnd()
+ const [modal,setModal] = useState(false)
+ const numPages = Math.ceil(count / rows)
+ const filterCnt = getFilterCount()
- // next/previous page button
- function handleTablePageChange(
- event: MouseEvent | null,
- newPage: number,
- ) {
- const url = ssrSoftwareUrl({
- // take existing params from url (query)
- ...ssrSoftwareParams(router.query),
- page: newPage,
- })
- router.push(url)
- }
+ // console.group('SoftwareHighlightsPage')
+ // console.log('search...', search)
+ // console.log('keywords...', keywords)
+ // console.log('prog_lang...', prog_lang)
+ // console.log('licenses...', licenses)
+ // console.log('order...', order)
+ // console.log('page...', page)
+ // console.log('rows...', rows)
+ // console.log('count...', count)
+ // console.log('layout...', layout)
+ // console.log('keywordsList...', keywordsList)
+ // console.log('languagesList...', languagesList)
+ // console.log('licensesList...', licensesList)
+ // console.log('software...', software)
+ // console.log('highlights...', highlights)
+ // console.groupEnd()
- function handlePaginationChange(
- event: ChangeEvent,
- newPage: number,
- ) {
- // Pagination component starts counting from 1, but we need to start from 0
- handleTablePageChange(event as any, newPage - 1)
- }
+ // Update view state based on layout value from cookie
+ useEffect(() => {
+ if (layout) {
+ setView(layout)
+ }
+ },[layout])
- // change number of cards per page
- function handleItemsPerPage(
- event: ChangeEvent,
- ){
- const url = ssrSoftwareUrl({
- // take existing params from url (query)
- ...ssrSoftwareParams(router.query),
- // reset to first page
- page: 0,
- rows: parseInt(event.target.value),
- })
- router.push(url)
+ function getFilterCount() {
+ let count = 0
+ // if (order) count++
+ if (keywords) count++
+ if (prog_lang) count++
+ if (licenses) count++
+ if (search) count++
+ return count
}
- function handleSearch(searchFor: string) {
- // debugger
- const url = ssrSoftwareUrl({
- // take existing params from url (query)
- ...ssrSoftwareParams(router.query),
- search: searchFor,
- // start from first page
- page: 0,
- })
- router.push(url)
+ function setLayout(view: LayoutType) {
+ // update local view
+ setView(view)
+ // save to cookie
+ setDocumentCookie(view,'rsd_page_layout')
}
- function handleFilters({keywords,prog_lang}:{keywords:string[],prog_lang:string[]}){
- const url = ssrSoftwareUrl({
- // take existing params from url (query)
- ...ssrSoftwareParams(router.query),
- keywords,
- prog_lang,
- // start from first page
- page: 0,
- })
- router.push(url)
- }
-
- // TODO! handle sort options
- // function handleSort(sortOn:string){
- // logger(`software.index.handleSort: TODO! Sort on...${sortOn}`,'warn')
- // }
-
return (
-
+ <>
{/* Page Head meta tags */}
{/* canonical url meta tag */}
-
-
-
-
-
-
+
+
+ {/* App header */}
+
+ {/* Software Highlights Carousel */}
+
+ {/* Main page body */}
+
+ {/* Page title */}
+
+ All software
+
+ {/* Page grid with 2 sections: left filter panel and main content */}
+
+ {/* Filters panel large screen */}
+ {smallScreen===false &&
+
+
+
+ }
+ {/* Search & main content section */}
+
+
+ {/* Software content: cards or list */}
+
+ {/* Pagination */}
+
+ {numPages > 1 &&
+
{
+ handleQueryChange('page',page.toString())
+ }}
+ />
+ }
+
+
-
-
-
-
-
-
-
-
+
+
+ {/* filter for mobile */}
+ {
+ smallScreen===true &&
+
-
-
+ }
+ >
)
}
// fetching data server side
// see documentation https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering
-export async function getServerSideProps(context:GetServerSidePropsContext) {
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ let orderBy, offset=0
// extract params from page-query
- const {search, keywords, prog_lang, rows, page} = ssrSoftwareParams(context.query)
+ const {search, keywords, prog_lang, licenses, order, rows, page} = ssrSoftwareParams(context.query)
// extract user settings from cookie
- const {rsd_page_rows} = getUserSettings(context.req)
+ const {rsd_page_layout, rsd_page_rows} = getUserSettings(context.req)
+ // default rows values comes from user settings
+ let page_rows = rsd_page_rows
+
+ if (order) {
+ // extract order direction from definitions
+ const orderInfo = softwareOrderOptions.find(item=>item.key===order)
+ if (orderInfo) orderBy=`${order}.${orderInfo.direction}`
+ }
+ // if rows && page are provided as query params
+ if (rows && page) {
+ offset = rows * (page - 1)
+ // use rows provided as param
+ page_rows = rows
+ }
// construct postgREST api url with query params
const url = softwareListUrl({
baseUrl: getBaseUrl(),
search,
keywords,
+ licenses,
prog_lang,
- order: search ? undefined : 'mention_cnt.desc.nullslast,contributor_cnt.desc.nullslast,updated_at.desc.nullslast,brand_name.asc',
- limit: rows ?? rsd_page_rows,
- offset: rows && page ? rows * page : undefined,
+ order: orderBy,
+ limit: page_rows,
+ offset
})
// console.log('software...url...', url)
+ // console.log('order...', order)
+ // console.log('orderBy...', orderBy)
+ // console.log('page_rows...', page_rows)
- // get software list, we do not pass the token
- // when token is passed it will return not published items too
- const software = await getSoftwareList({url})
+ // get software items, filter options AND highlights
+ const [
+ software,
+ keywordsList,
+ languagesList,
+ licensesList,
+ // extract highlights from fn response (we don't need count)
+ {highlights}
+ ] = await Promise.all([
+ getSoftwareList({url}),
+ softwareKeywordsFilter({search, keywords, prog_lang, licenses}),
+ softwareLanguagesFilter({search, keywords, prog_lang, licenses}),
+ softwareLicesesFilter({search, keywords, prog_lang, licenses}),
+ getSoftwareHighlights({
+ page: 0,
+ // get max. 20 items
+ rows: 20,
+ orderBy: 'position'
+ })
+ ])
- // will be passed as props to page
- // see params of SoftwareIndexPage function
+ // passed as props to the page
+ // see params of page function
return {
props: {
- search: search ?? null,
- keywords: keywords ?? null,
- prog_lang: prog_lang ?? null,
- count: software.count,
+ search,
+ keywords,
+ keywordsList,
+ prog_lang,
+ languagesList,
+ licenses,
+ licensesList,
page,
- rows: rows ?? rsd_page_rows,
+ order,
+ rows: page_rows,
+ layout: rsd_page_layout,
+ count: software.count,
software: software.data,
+ highlights
},
}
}