From 5aca3a9ebe41824cce8d18ddc4e10341fe81eff2 Mon Sep 17 00:00:00 2001 From: "Dusan Mijatovic (PC2020)" Date: Fri, 12 May 2023 13:44:37 +0200 Subject: [PATCH] feat: new software overview layout including highlights and reactive filters in the filter panel. BREAKING CHANGE: bump version to v2. --- frontend/__tests__/SoftwareIndex.test.tsx | 75 --- frontend/__tests__/SoftwareOverview.test.tsx | 126 +++++ .../__mocks__/softwareOverviewData.json | 497 ++++++++++++++++++ .../softwareOverviewData.json.license | 4 + .../components/cards/CardTitleSubtitle.tsx | 24 + .../overview => cards}/KeywordList.tsx | 0 .../ProgrammingLanguageList.tsx | 0 .../overview => cards}/SoftwareMetrics.tsx | 2 +- .../layout/ImageWithPlaceholder.tsx | 17 +- frontend/components/mention/MentionNote.tsx | 6 +- .../software/highlights/HighlightsCard.tsx | 73 +++ .../highlights/HighlightsCarousel.tsx | 51 +- .../software/highlights/LeftButton.tsx | 23 + .../software/highlights/RightButton.tsx | 22 + .../software/overview/SearchInput.tsx | 1 - .../software/overview/SearchSection.tsx | 18 +- .../software/overview/SoftwareCard.tsx | 146 ----- .../overview/SoftwareFiltersPanel.tsx | 4 +- .../software/overview/SoftwareGridCard.tsx | 66 ++- .../SoftwareHighlights.tsx | 2 +- .../software/overview/SoftwareMasonryCard.tsx | 71 +++ .../overview/SoftwareOverviewContent.tsx | 5 + .../overview/SoftwareOverviewGrid.tsx | 3 +- .../overview/SoftwareOverviewList.tsx | 97 ++-- .../overview/SoftwareOverviewMasonry.tsx | 8 +- .../overview/filters/FilterHeader.tsx | 2 +- .../overview/filters/FilterOption.tsx | 25 + .../software/overview/filters/FilterTitle.tsx | 13 + .../overview/filters/KeywordsFilter.tsx | 25 +- .../overview/filters/LicensesFilter.tsx | 20 +- .../software/overview/filters/OrderBy.tsx | 24 +- .../filters/ProgrammingLanguagesFilter.tsx | 26 +- .../software/overview/filters/index.tsx | 3 - .../software/overview/useSoftwareParams.ts | 2 +- .../softwarePage/FeaturedSoftwareCarousel.tsx | 86 --- .../components/softwarePage/SearchInput.tsx | 62 --- .../components/softwarePage/SoftwareCard.tsx | 140 ----- .../softwarePage/SoftwareFilterPanel.tsx | 197 ------- .../softwarePage/softwarePagePanel.d.ts | 9 - .../softwarePage/useSoftwarefilterPanel.ts | 205 -------- frontend/next.config.js | 6 +- frontend/pages/highlights/index.tsx | 289 ---------- frontend/pages/software/index.tsx | 391 ++++++++------ 43 files changed, 1316 insertions(+), 1550 deletions(-) delete mode 100644 frontend/__tests__/SoftwareIndex.test.tsx create mode 100644 frontend/__tests__/SoftwareOverview.test.tsx create mode 100644 frontend/__tests__/__mocks__/softwareOverviewData.json create mode 100644 frontend/__tests__/__mocks__/softwareOverviewData.json.license create mode 100644 frontend/components/cards/CardTitleSubtitle.tsx rename frontend/components/{software/overview => cards}/KeywordList.tsx (100%) rename frontend/components/{software/overview => cards}/ProgrammingLanguageList.tsx (100%) rename frontend/components/{software/overview => cards}/SoftwareMetrics.tsx (95%) create mode 100644 frontend/components/software/highlights/HighlightsCard.tsx create mode 100644 frontend/components/software/highlights/LeftButton.tsx create mode 100644 frontend/components/software/highlights/RightButton.tsx delete mode 100644 frontend/components/software/overview/SoftwareCard.tsx rename frontend/components/software/{highlights => overview}/SoftwareHighlights.tsx (91%) create mode 100644 frontend/components/software/overview/SoftwareMasonryCard.tsx create mode 100644 frontend/components/software/overview/filters/FilterOption.tsx create mode 100644 frontend/components/software/overview/filters/FilterTitle.tsx delete mode 100644 frontend/components/softwarePage/FeaturedSoftwareCarousel.tsx delete mode 100644 frontend/components/softwarePage/SearchInput.tsx delete mode 100644 frontend/components/softwarePage/SoftwareCard.tsx delete mode 100644 frontend/components/softwarePage/SoftwareFilterPanel.tsx delete mode 100644 frontend/components/softwarePage/softwarePagePanel.d.ts delete mode 100644 frontend/components/softwarePage/useSoftwarefilterPanel.ts delete mode 100644 frontend/pages/highlights/index.tsx 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} +

+
+

+ {subtitle} +

+
+ + ) +} 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 && + {`Cover + } + + {/* 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 */}
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 && - {`Cover - } - - {/* 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 && ( -
- - - - - 34K -
- )} -
-
-
-
- - ) -} -// 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 && -
- {`Cover -
- } - {/* Card body - 60% of card height */} -
-

- {item.brand_name} -

-
-

- {item.short_statement} -

-
-
- {/* keywords */} -
- +
+ {/* Card body - 67% of card height */} +
+ + + {/* keywords */} +
+ +
- {/*
*/}
{/* Languages */}
- + ) } 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 && + {`Cover + } + + {/* 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 - {`Cover} - -
-
-
+
+ {software.map(item => ( + +
+ {item.image_id ? + {`Cover + : +
+ } +
+
+
{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'}
- } - - {/* 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 */} - {`Cover - - {/* 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 && ( -
- - - - - 34K -
- )} -
-
-
-
- - ) -} -// 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 -
- - -
- - {/* Order by */} - - Order by - - - - {/* 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 }, } }