From 56efe2bf9451b4350865a1b3dda25bf863ff069d Mon Sep 17 00:00:00 2001 From: "Dusan Mijatovic (PC2020)" Date: Tue, 3 Sep 2024 20:13:30 +0200 Subject: [PATCH] feat: add testimonials to project pages --- data-generation/main.js | 21 ++ .../007-create-relations-for-projects.sql | 31 ++ .../projects/edit/editProjectPages.tsx | 12 + .../EditProjectTestimonialsIndex.test.tsx | 268 ++++++++++++++++++ .../__mocks__/project_testimonials.json | 16 ++ .../project_testimonials.json.license | 4 + .../testimonials/apiProjectTestimonial.ts | 152 ++++++++++ .../projects/edit/testimonials/config.ts | 27 ++ .../projects/edit/testimonials/index.tsx | 191 +++++++++++++ .../testimonials/useProjectTestimonial.tsx | 148 ++++++++++ .../software/edit/editSoftwareConfig.tsx | 1 - .../EditSoftwareTestimonialsIndex.test.tsx | 5 +- .../testimonials/EditTestimonialModal.tsx | 10 +- .../testimonials/apiSoftwareTestimonial.ts} | 41 +-- .../software/edit/testimonials/index.tsx | 175 ++++-------- .../testimonials/useSoftwareTestimonials.tsx | 150 ++++++++++ .../edit/testimonials/useTestimonials.tsx | 46 --- frontend/pages/projects/[slug]/index.tsx | 17 +- frontend/pages/software/[slug]/index.tsx | 5 +- frontend/types/Testimonial.ts | 4 +- frontend/utils/fetchHelpers.ts | 70 +++-- 21 files changed, 1165 insertions(+), 229 deletions(-) create mode 100644 frontend/components/projects/edit/testimonials/EditProjectTestimonialsIndex.test.tsx create mode 100644 frontend/components/projects/edit/testimonials/__mocks__/project_testimonials.json create mode 100644 frontend/components/projects/edit/testimonials/__mocks__/project_testimonials.json.license create mode 100644 frontend/components/projects/edit/testimonials/apiProjectTestimonial.ts create mode 100644 frontend/components/projects/edit/testimonials/config.ts create mode 100644 frontend/components/projects/edit/testimonials/index.tsx create mode 100644 frontend/components/projects/edit/testimonials/useProjectTestimonial.tsx rename frontend/{utils/editTestimonial.ts => components/software/edit/testimonials/apiSoftwareTestimonial.ts} (72%) create mode 100644 frontend/components/software/edit/testimonials/useSoftwareTestimonials.tsx delete mode 100644 frontend/components/software/edit/testimonials/useTestimonials.tsx diff --git a/data-generation/main.js b/data-generation/main.js index 590d81f24..818d77f0e 100644 --- a/data-generation/main.js +++ b/data-generation/main.js @@ -151,6 +151,26 @@ function generateTestimonials(ids) { software: id, message: faker.hacker.phrase(), source: faker.person.fullName(), + position: index + 1, + }); + } + } + + return result; +} + +function generateProjectTestimonials(ids) { + const result = []; + + for (const id of ids) { + // each project will get 0, 1 or 2 testimonials + const numberOfTestimonials = faker.number.int({max: 3, min: 0}); + for (let index = 0; index < numberOfTestimonials; index++) { + result.push({ + project: id, + message: faker.hacker.phrase(), + source: faker.person.fullName(), + position: index + 1, }); } } @@ -1019,6 +1039,7 @@ const projectPromise = postToBackend('/project', generateProjects()) idsProjects = pjArray.map(sw => sw['id']); postToBackend('/team_member', await generateTeamMembers(idsProjects, peopleWithOrcid)); postToBackend('/url_for_project', generateUrlsForProjects(idsProjects)); + postToBackend('/testimonial_for_project', generateProjectTestimonials(idsProjects)); postToBackend('/keyword_for_project', generateKeywordsForEntity(idsProjects, idsKeywords, 'project')); postToBackend('/output_for_project', generateMentionsForEntity(idsProjects, idsMentions, 'project')); postToBackend('/impact_for_project', generateMentionsForEntity(idsProjects, idsMentions, 'project')); diff --git a/database/007-create-relations-for-projects.sql b/database/007-create-relations-for-projects.sql index 9ac74fa95..9dce26daf 100644 --- a/database/007-create-relations-for-projects.sql +++ b/database/007-create-relations-for-projects.sql @@ -2,6 +2,7 @@ -- SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center -- SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -- SPDX-FileCopyrightText: 2022 dv4all +-- SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) -- -- SPDX-License-Identifier: Apache-2.0 @@ -45,3 +46,33 @@ END $$; CREATE TRIGGER sanitise_update_team_member BEFORE UPDATE ON team_member FOR EACH ROW EXECUTE PROCEDURE sanitise_update_team_member(); + + +CREATE TABLE testimonial_for_project ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + project UUID REFERENCES project (id) NOT NULL, + message VARCHAR(500) NOT NULL, + source VARCHAR(200) NOT NULL, + position INTEGER +); + +CREATE FUNCTION sanitise_insert_testimonial_for_project() RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + NEW.id = gen_random_uuid(); + return NEW; +END +$$; + +CREATE TRIGGER sanitise_insert_testimonial_for_project BEFORE INSERT ON testimonial_for_project FOR EACH ROW EXECUTE PROCEDURE sanitise_insert_testimonial_for_project(); + + +CREATE FUNCTION sanitise_update_testimonial_for_project() RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + NEW.id = OLD.id; + return NEW; +END +$$; + +CREATE TRIGGER sanitise_update_testimonial_for_project BEFORE UPDATE ON testimonial_for_project FOR EACH ROW EXECUTE PROCEDURE sanitise_update_testimonial_for_project(); diff --git a/frontend/components/projects/edit/editProjectPages.tsx b/frontend/components/projects/edit/editProjectPages.tsx index b47051372..7652c5669 100644 --- a/frontend/components/projects/edit/editProjectPages.tsx +++ b/frontend/components/projects/edit/editProjectPages.tsx @@ -16,6 +16,7 @@ import TerminalIcon from '@mui/icons-material/Terminal' import ContentLoader from '~/components/layout/ContentLoader' import JoinInnerIcon from '@mui/icons-material/JoinInner' import AddCommentIcon from '@mui/icons-material/AddComment' +import ThreePIcon from '@mui/icons-material/ThreeP' // use dynamic imports const ProjectInformation = dynamic(() => import('./information'),{ @@ -36,6 +37,10 @@ const RelatedProjects = dynamic(() => import('./related-projects'),{ const RelatedSoftware = dynamic(() => import('./related-software'),{ loading: ()=> }) +const ProjectTestimonials = dynamic(() => import('./testimonials'),{ + loading: ()=> +}) + const ProjectMaintainers = dynamic(() => import('./maintainers'),{ loading: ()=> }) @@ -77,6 +82,13 @@ export const editProjectPage: EditProjectPageProps[] = [ render: () => , status: '' }, + { + id: 'testimonials', + label: 'Testimonials', + icon: , + render: () => , + status: '' + }, { id: 'related-projects', label: 'Related projects', diff --git a/frontend/components/projects/edit/testimonials/EditProjectTestimonialsIndex.test.tsx b/frontend/components/projects/edit/testimonials/EditProjectTestimonialsIndex.test.tsx new file mode 100644 index 000000000..85b8711ac --- /dev/null +++ b/frontend/components/projects/edit/testimonials/EditProjectTestimonialsIndex.test.tsx @@ -0,0 +1,268 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) +// SPDX-FileCopyrightText: 2023 dv4all +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {fireEvent, render, screen, waitFor, waitForElementToBeRemoved, within} from '@testing-library/react' + +import {WithAppContext, mockSession} from '~/utils/jest/WithAppContext' +import {WithProjectContext} from '~/utils/jest/WithProjectContext' +import {initialState as projectState} from '~/components/projects/edit/editProjectContext' + +import SoftwareTestimonials from './index' +import config from './config' + +// MOCKS +import mockTestimonials from './__mocks__/project_testimonials.json' + +// Mock editTestimonial api calls +const mockGetTestimonialsForProject = jest.fn(props => Promise.resolve(mockTestimonials)) +const mockPostTestimonial = jest.fn(({testimonial}) => { + return Promise.resolve({ + status: 201, + message: testimonial + }) +}) +const mockDeleteProjectTestimonial = jest.fn(props => Promise.resolve([] as any)) +const mockPatchTestimonialPositions = jest.fn(props => Promise.resolve([] as any)) +jest.mock('./apiProjectTestimonial', () => ({ + getTestimonialsForProject: jest.fn(props => mockGetTestimonialsForProject(props)), + addProjectTestimonial: jest.fn(props => mockPostTestimonial(props)), + deleteProjectTestimonial: jest.fn(props => mockDeleteProjectTestimonial(props)), + patchTestimonialPositions: jest.fn(props => mockPatchTestimonialPositions(props)), +})) + +describe('frontend/components/projects/edit/testimonials/index.tsx', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('renders no testimonials message', async() => { + // required prop + projectState.project.id = 'test-project-id' + // return no items + mockGetTestimonialsForProject.mockResolvedValueOnce([]) + + render( + + + + + + ) + + // wait for loader to + await waitForElementToBeRemoved(screen.getByRole('progressbar')) + // validate no items message + const noItemsMsg = screen.getByText('No testimonials') + }) + + it('renders mocked testimonials', async() => { + // required prop + projectState.project.id = 'test-project-id' + // return no items + mockGetTestimonialsForProject.mockResolvedValueOnce(mockTestimonials) + + render( + + + + + + ) + + // wait for loader to + await waitForElementToBeRemoved(screen.getByRole('progressbar')) + + // validate number of items + const testimonials = screen.getAllByTestId('testimonial-list-item') + expect(testimonials.length).toEqual(mockTestimonials.length) + // validate first item message + expect(testimonials[0]).toHaveTextContent(mockTestimonials[0].message) + }) + + it('can add testimonial', async() => { + // required prop + projectState.project.id = 'test-project-id' + // return no items + mockGetTestimonialsForProject.mockResolvedValueOnce([]) + + const newItem = { + message: 'This is test message', + source: 'This is test source' + } + + render( + + + + + + ) + + // wait for loader to + await waitForElementToBeRemoved(screen.getByRole('progressbar')) + + // click add button + const addBtn = screen.getByRole('button', { + name: 'Add' + }) + fireEvent.click(addBtn) + + // get modal + const modal = screen.getByRole('dialog') + + // write message + const message = screen.getByRole('textbox', { + name: config.message.label, + }) + fireEvent.change(message, {target: {value: newItem.message}}) + + // write source + const source = screen.getByRole('textbox', { + name: config.source.label, + }) + fireEvent.change(source, {target: {value: newItem.source}}) + + // click on save + const saveBtn = screen.getByRole('button', { + name: 'Save' + }) + await waitFor(() => { + expect(saveBtn).toBeEnabled() + fireEvent.click(saveBtn) + }) + + // validate api call + await waitFor(() => { + expect(mockPostTestimonial).toBeCalledTimes(1) + expect(mockPostTestimonial).toBeCalledWith({ + 'testimonial': { + 'id': null, + 'message': newItem.message, + 'position': 1, + 'project': projectState.project.id, + 'source': newItem.source, + }, + 'token': 'TEST_TOKEN' + }) + }) + }) + + it('can edit testimonial', async() => { + // required prop + projectState.project.id = 'test-project-id' + // return no items + mockGetTestimonialsForProject.mockResolvedValueOnce(mockTestimonials) + + render( + + + + + + ) + + // wait for loader to + await waitForElementToBeRemoved(screen.getByRole('progressbar')) + + // get items + const testimonials = screen.getAllByTestId('testimonial-list-item') + // edit btn + const editBtn = within(testimonials[0]).getByRole('button', { + name: 'edit' + }) + fireEvent.click(editBtn) + + // confirm modal has values + const modal = screen.getByRole('dialog') + + // validate message value + const message = screen.getByRole('textbox', { + name: config.message.label, + }) + expect(message).toHaveValue(mockTestimonials[0].message) + + // validate source value + const source = screen.getByRole('textbox', { + name: config.source.label, + }) + expect(source).toHaveValue(mockTestimonials[0].source) + + // Cancel update action + const cancelBtn = screen.getByRole('button', { + name: 'Cancel' + }) + fireEvent.click(cancelBtn) + // validate modal is closed + expect(modal).not.toBeVisible() + }) + + it('can delete testimonial', async () => { + // required prop + projectState.project.id = 'test-project-id' + // return no items + mockGetTestimonialsForProject.mockResolvedValueOnce(mockTestimonials) + // mock delete response + mockDeleteProjectTestimonial.mockResolvedValueOnce({ + status: 200, + message: 'OK' + }) + // mock patch response + mockPatchTestimonialPositions.mockResolvedValueOnce({ + status: 200, + message: 'OK' + }) + + render( + + + + + + ) + + // wait for loader to + await waitForElementToBeRemoved(screen.getByRole('progressbar')) + + // get items + const testimonials = screen.getAllByTestId('testimonial-list-item') + // edit btn + const deleteBtn = within(testimonials[0]).getByRole('button', { + name: 'delete' + }) + fireEvent.click(deleteBtn) + + // confirm modal + const confirmModal = screen.getByRole('dialog', { + name: 'Remove testimonial' + }) + // click on Remove button + const removeBtn = within(confirmModal).getByRole('button', { + name: 'Remove' + }) + fireEvent.click(removeBtn) + + // validate api calls + await waitFor(() => { + // validate delete testimonial api + expect(mockDeleteProjectTestimonial).toBeCalledTimes(1) + expect(mockDeleteProjectTestimonial).toBeCalledWith({ + 'id': mockTestimonials[0].id, + 'token': mockSession.token, + }) + // validate patch testimonial positions called + expect(mockPatchTestimonialPositions).toBeCalledTimes(1) + }) + + await waitFor(async() => { + // validate item removed from list + const remained = await screen.findAllByTestId('testimonial-list-item') + expect(remained.length).toEqual(testimonials.length - 1) + }) + }) +}) + diff --git a/frontend/components/projects/edit/testimonials/__mocks__/project_testimonials.json b/frontend/components/projects/edit/testimonials/__mocks__/project_testimonials.json new file mode 100644 index 000000000..a8529b7a4 --- /dev/null +++ b/frontend/components/projects/edit/testimonials/__mocks__/project_testimonials.json @@ -0,0 +1,16 @@ +[ + { + "id": "eec59b21-1fb6-4e63-830b-fcf837c812c1", + "project": "2930a586-6781-4091-af8e-7411f9be76d4", + "message": "This is my testimonal for msedge 1", + "source": "Me, Here, Today", + "position": 1 + }, + { + "id": "e53e1572-0aad-4b5d-817b-219b20138c57", + "project": "2930a586-6781-4091-af8e-7411f9be76d4", + "message": "This is my testimonal for msedge 2", + "source": "You, There, Yesterday", + "position": 2 + } +] \ No newline at end of file diff --git a/frontend/components/projects/edit/testimonials/__mocks__/project_testimonials.json.license b/frontend/components/projects/edit/testimonials/__mocks__/project_testimonials.json.license new file mode 100644 index 000000000..f5831b46e --- /dev/null +++ b/frontend/components/projects/edit/testimonials/__mocks__/project_testimonials.json.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +SPDX-FileCopyrightText: 2024 Netherlands eScience Center + +SPDX-License-Identifier: Apache-2.0 diff --git a/frontend/components/projects/edit/testimonials/apiProjectTestimonial.ts b/frontend/components/projects/edit/testimonials/apiProjectTestimonial.ts new file mode 100644 index 000000000..77cfb7ecb --- /dev/null +++ b/frontend/components/projects/edit/testimonials/apiProjectTestimonial.ts @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {NewTestimonial, Testimonial} from '~/types/Testimonial' +import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' +import logger from '~/utils/logger' + +export type NewProjectTestimonial = NewTestimonial & { + project: string +} + +export type ProjectTestimonial = NewProjectTestimonial & { + id: string + position: number +} + +export async function getTestimonialsForProject({project,token}:{project:string,token:string}){ + try{ + const url = `${getBaseUrl()}/testimonial_for_project?project=eq.${project}&order=position` + const resp = await fetch(url, { + method: 'GET', + headers: { + ...createJsonHeaders(token), + }, + }) + if (resp.ok){ + const testimonials:ProjectTestimonial[] = await resp.json() + return testimonials + } + logger(`getTestimonialsForProject: ${resp.status} ${resp.statusText}`, 'warn') + return [] + }catch(e:any){ + logger(`getTestimonialsForProject: ${e?.message}`, 'error') + return [] + } +} + +export async function addProjectTestimonial({testimonial,token}:{testimonial:NewProjectTestimonial,token:string}){ + try{ + const url = `${getBaseUrl()}/testimonial_for_project` + const resp = await fetch(url, { + method: 'POST', + headers: { + ...createJsonHeaders(token), + 'Prefer': 'return=representation', + }, + body: JSON.stringify(testimonial) + }) + if (resp.status === 201) { + const json = await resp.json() + if (json.length > 0) { + // we return stored record + return { + status: 201, + message: json[0] + } + } else { + logger('addProjectTestimonial: resp.json() returned no records', 'error') + return { + status: 400, + message: 'Bad request' + } + } + } else { + return extractReturnMessage(resp) + } + }catch(e:any){ + logger(`addProjectTestimonial: ${e?.message}`, 'error') + return { + status: 500, + message: e.message + } + } +} + +export async function updateProjectTestimonial({data,token}:{data:ProjectTestimonial,token:string}){ + try{ + const url = `${getBaseUrl()}/testimonial_for_project?id=eq.${data.id}` + const resp = await fetch(url, { + method: 'PATCH', + headers: { + ...createJsonHeaders(token), + }, + body: JSON.stringify(data) + }) + return extractReturnMessage(resp,data.id) + }catch(e:any){ + logger(`patchProjectTestimonial: ${e?.message}`, 'error') + return { + status: 500, + message: e.message + } + } +} + +export async function deleteProjectTestimonial({id,token}:{id:string,token:string}){ + try { + const url = `${getBaseUrl()}/testimonial_for_project?id=eq.${id}` + const resp = await fetch(url, { + method: 'DELETE', + headers: { + ...createJsonHeaders(token), + } + }) + return extractReturnMessage(resp, id) + } catch (e: any) { + logger(`deleteProjectTestimonial: ${e?.message}`, 'error') + return { + status: 500, + message: e?.message + } + } +} + +/** + * Patch testimonial position. + * For this we do not need project id, we patch by testimonial id. + * @param param0 + * @returns + */ +export async function patchTestimonialPositions({testimonials, token}: { testimonials: Testimonial[], token: string }) { + try { + // if the array is empty return + if (testimonials.length === 0) return {status:200,message:'OK'} + // create all requests + const requests = testimonials.map(testimonial => { + const url = `${getBaseUrl()}/testimonial_for_project?id=eq.${testimonial.id}` + return fetch(url, { + method: 'PATCH', + headers: { + ...createJsonHeaders(token), + }, + // just update position! + body: JSON.stringify({ + position: testimonial.position + }) + }) + }) + // execute them in parallel + const responses = await Promise.all(requests) + // check for errors + return extractReturnMessage(responses[0]) + } catch (e: any) { + logger(`patchTestimonialPositions: ${e?.message}`, 'error') + return { + status: 500, + message: e?.message + } + } +} diff --git a/frontend/components/projects/edit/testimonials/config.ts b/frontend/components/projects/edit/testimonials/config.ts new file mode 100644 index 000000000..1436b5994 --- /dev/null +++ b/frontend/components/projects/edit/testimonials/config.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +const config = { + message: { + label: 'Message', + help: 'What credits the project received?', + validation: { + required: 'The message is required', + minLength: {value: 2, message: 'Minimum length is 2'}, + maxLength: {value: 500, message: 'Maximum length is 500'}, + } + }, + source: { + label: 'Source', + help: 'Who provided the credits?', + validation: { + required: 'The source of the testimonial is required', + minLength: {value: 2, message: 'Minimum length is 2'}, + maxLength: {value: 200, message: 'Maximum length is 200'}, + } + } +} + +export default config diff --git a/frontend/components/projects/edit/testimonials/index.tsx b/frontend/components/projects/edit/testimonials/index.tsx new file mode 100644 index 000000000..40eee2c15 --- /dev/null +++ b/frontend/components/projects/edit/testimonials/index.tsx @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useState} from 'react' +import AddIcon from '@mui/icons-material/Add' +import Button from '@mui/material/Button' + +import {NewTestimonial, Testimonial} from '~/types/Testimonial' +import EditSection from '~/components/layout/EditSection' +import EditSectionTitle from '~/components/layout/EditSectionTitle' +import ContentLoader from '~/components/layout/ContentLoader' +import ConfirmDeleteModal from '~/components/layout/ConfirmDeleteModal' +import SortableTestimonialList from '~/components/software/edit/testimonials/SortableTestimonialList' +import EditTestimonialModal from '~/components/software/edit/testimonials/EditTestimonialModal' +import {ModalProps} from '~/components/software/edit/editSoftwareTypes' +import {NewProjectTestimonial,ProjectTestimonial} from './apiProjectTestimonial' +import useProjectTestimonial from './useProjectTestimonial' +import config from './config' + +type EditTestimonialModal = ModalProps & { + testimonial?: NewProjectTestimonial | ProjectTestimonial +} + +export default function ProjectTestimonials() { + const { + loading, testimonials, project, + addTestimonial, updateTestimonial, + deleteTestimonial,updateTestimonialPosition + } = useProjectTestimonial() + + const [modal,setModal] = useState<{ + edit:{ + open:boolean, + pos?: number, + }, + delete:{ + open:boolean + }, + testimonial?: NewTestimonial | Testimonial + }>({ + edit:{ + open:false, + pos: 0 + }, + delete:{ + open:false, + } + }) + + // console.group("ProjectTestimonials") + // console.log('testimonials...', testimonials) + // console.log("loading...", loading) + // console.groupEnd() + + function onAdd(){ + setModal({ + edit:{ + open: true, + }, + delete:{ + open: false + }, + testimonial:{ + id: null, + message: null, + source: null, + position: testimonials.length + 1 + } + }) + } + + function onEdit(pos:number){ + setModal({ + edit:{ + open: true, + pos + }, + delete:{ + open:false + }, + testimonial: testimonials[pos] + }) + } + + function onDelete(pos:number){ + setModal({ + edit:{ + open: false, + pos + }, + delete:{ + open:true + }, + testimonial: testimonials[pos] + }) + } + + function onSubmit({data,pos}:{data:Testimonial|NewTestimonial,pos?:number}){ + closeModals() + + if (data.id === null){ + // create new item + addTestimonial(data) + }else if (typeof pos === 'number'){ + // update item + updateTestimonial({data: data as Testimonial, pos}) + } + } + + function getTestimonialSubtitle() { + if (testimonials?.length === 1) { + return `${project?.title} has 1 testimonial` + } + return `${project?.title} has ${testimonials?.length} testimonials` + } + + function closeModals(){ + setModal({ + edit:{ + open: false, + pos: 0 + }, + delete:{ + open: false + } + }) + } + + // if loading show loader + if (loading) return ( + + ) + + return ( +
+ +
+ + + + +
+
+ {modal.edit.open ? + + : null + } + {modal.delete.open ? + Are you sure you want to remove testimonial from source {modal?.testimonial?.source ?? ''}?

+ } + onCancel={closeModals} + onDelete={()=>{ + if (modal?.testimonial?.id){ + deleteTestimonial(modal?.testimonial?.id) + } + closeModals() + }} + /> + : null + } +
+ ) +} diff --git a/frontend/components/projects/edit/testimonials/useProjectTestimonial.tsx b/frontend/components/projects/edit/testimonials/useProjectTestimonial.tsx new file mode 100644 index 000000000..3d3e7e8bb --- /dev/null +++ b/frontend/components/projects/edit/testimonials/useProjectTestimonial.tsx @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useCallback, useEffect, useState} from 'react' + +import {useSession} from '~/auth' +import {NewTestimonial, Testimonial} from '~/types/Testimonial' +import useSnackbar from '~/components/snackbar/useSnackbar' +import useProjectContext from '../useProjectContext' +import {addProjectTestimonial, deleteProjectTestimonial, + getTestimonialsForProject, patchTestimonialPositions, + updateProjectTestimonial +} from './apiProjectTestimonial' +import {sortOnNumProp} from '~/utils/sortFn' + +export default function useProjectTestimonial(){ + const {token} = useSession() + const {project} = useProjectContext() + const [loading, setLoading] = useState(true) + const [testimonials, setTestimonials] = useState([]) + const {showErrorMessage} = useSnackbar() + + const getTestimonials = useCallback(async()=>{ + if (project.id && token){ + const testimonials = await getTestimonialsForProject({ + project: project.id, + token + }) + setTestimonials(testimonials) + setLoading(false) + } + },[project.id,token]) + + const addTestimonial = useCallback(async(item:NewTestimonial)=>{ + if (project.id && token){ + const resp = await addProjectTestimonial({ + testimonial:{ + ...item, + project: project.id + }, + token + }) + if (resp.status!==201){ + showErrorMessage(`Failed to add testimonial. ${resp.message}`) + }else{ + // add new item + setTestimonials((data)=>{ + return [ + ...data, + resp.message + ] + }) + } + } + // ignore showErrorMessage + // eslint-disable-next-line react-hooks/exhaustive-deps + },[token,project.id]) + + const updateTestimonial = useCallback(async({data, pos}: { data: Testimonial, pos: number })=>{ + if (project.id && token && data.id){ + const testimonial = { + ...data, + project: project.id + } + const resp = await updateProjectTestimonial({ + data:testimonial, + token + }) + if (resp.status==200){ + setTestimonials((testimonials)=>{ + // replace item in state + const list = [ + ...testimonials.slice(0, pos), + testimonial, + ...testimonials.slice(pos+1) + ].sort((a,b)=>sortOnNumProp(a,b,'position')) + return list + }) + }else{ + showErrorMessage(`Failed to update testimonial. ${resp.message}`) + } + } + // ignore showErrorMessage as dependency + // eslint-disable-next-line react-hooks/exhaustive-deps + },[token,project.id]) + + const deleteTestimonial = useCallback(async(id:string)=>{ + if (token && id){ + const resp = await deleteProjectTestimonial({ + id, + token + }) + if (resp.status===200){ + // remove item from the list + const list = testimonials + .filter(item=>item.id!==id) + .map((item,pos) => { + item.position = pos + 1 + return item + }) + // patch testimonials position + await updateTestimonialPosition(list) + }else{ + showErrorMessage(`Failed to delete testimonial. ${resp.message}`) + } + } + // ignore showErrorMessage as dependency + // eslint-disable-next-line react-hooks/exhaustive-deps + },[token,testimonials]) + + const updateTestimonialPosition = useCallback(async(newList: Testimonial[])=>{ + // update ui first + setTestimonials(newList) + + // update db + const resp = await patchTestimonialPositions({ + testimonials: newList, + token + }) + + if (resp.status!==200){ + // revert back + setTestimonials(testimonials) + showErrorMessage(`Failed to update positions. ${resp.message}`) + } + // ignore showErrorMessage as dependency + // eslint-disable-next-line react-hooks/exhaustive-deps + },[token,testimonials]) + + + useEffect(()=>{ + if (project.id && token){ + getTestimonials() + } + },[project.id,token,getTestimonials]) + + return { + loading, + testimonials, + project, + addTestimonial, + updateTestimonial, + updateTestimonialPosition, + deleteTestimonial + } +} diff --git a/frontend/components/software/edit/editSoftwareConfig.tsx b/frontend/components/software/edit/editSoftwareConfig.tsx index cda5138b5..e29af8349 100644 --- a/frontend/components/software/edit/editSoftwareConfig.tsx +++ b/frontend/components/software/edit/editSoftwareConfig.tsx @@ -235,7 +235,6 @@ export const testimonialInformation = { } } - export const mentionInformation = { sectionTitle: 'Mentions', mentionType: { diff --git a/frontend/components/software/edit/testimonials/EditSoftwareTestimonialsIndex.test.tsx b/frontend/components/software/edit/testimonials/EditSoftwareTestimonialsIndex.test.tsx index 3f6c2c3bb..506a1c274 100644 --- a/frontend/components/software/edit/testimonials/EditSoftwareTestimonialsIndex.test.tsx +++ b/frontend/components/software/edit/testimonials/EditSoftwareTestimonialsIndex.test.tsx @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) // SPDX-FileCopyrightText: 2023 dv4all +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -26,14 +28,13 @@ const mockPostTestimonial = jest.fn(({testimonial}) => { }) const mockDeleteTestimonialById = jest.fn(props => Promise.resolve([] as any)) const mockPatchTestimonialPositions = jest.fn(props => Promise.resolve([] as any)) -jest.mock('~/utils/editTestimonial', () => ({ +jest.mock('./apiSoftwareTestimonial', () => ({ getTestimonialsForSoftware: jest.fn(props => mockGetTestimonialsForSoftware(props)), postTestimonial: jest.fn(props => mockPostTestimonial(props)), deleteTestimonialById: jest.fn(props => mockDeleteTestimonialById(props)), patchTestimonialPositions: jest.fn(props => mockPatchTestimonialPositions(props)), })) - describe('frontend/components/software/edit/testimonials/index.tsx', () => { beforeEach(() => { jest.clearAllMocks() diff --git a/frontend/components/software/edit/testimonials/EditTestimonialModal.tsx b/frontend/components/software/edit/testimonials/EditTestimonialModal.tsx index 92ecee567..37bb9ac84 100644 --- a/frontend/components/software/edit/testimonials/EditTestimonialModal.tsx +++ b/frontend/components/software/edit/testimonials/EditTestimonialModal.tsx @@ -3,6 +3,8 @@ // SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ) // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) (dv4all) // SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -16,11 +18,11 @@ import useMediaQuery from '@mui/material/useMediaQuery' import {useForm} from 'react-hook-form' import ControlledTextField from '../../../form/ControlledTextField' -import {testimonialInformation as config} from '../editSoftwareConfig' import {NewTestimonial, Testimonial} from '../../../../types/Testimonial' import SubmitButtonWithListener from '~/components/form/SubmitButtonWithListener' type EditTestimonialModalProps = { + config: any, open: boolean, onCancel: () => void, onSubmit: ({data, pos}: { data: Testimonial|NewTestimonial, pos?: number }) => void, @@ -31,7 +33,7 @@ type EditTestimonialModalProps = { const formId='edit-testimonial-modal' -export default function EditTestimonialModal({open, onCancel, onSubmit, testimonial, pos}: EditTestimonialModalProps) { +export default function EditTestimonialModal({config,open,onCancel,onSubmit,testimonial,pos}: EditTestimonialModalProps) { const smallScreen = useMediaQuery('(max-width:600px)') const {handleSubmit, watch, formState, reset, control, register, setValue} = useForm({ mode: 'onChange', @@ -83,9 +85,9 @@ export default function EditTestimonialModal({open, onCancel, onSubmit, testimon - + /> */} diff --git a/frontend/utils/editTestimonial.ts b/frontend/components/software/edit/testimonials/apiSoftwareTestimonial.ts similarity index 72% rename from frontend/utils/editTestimonial.ts rename to frontend/components/software/edit/testimonials/apiSoftwareTestimonial.ts index df7781d0d..3b5dcd765 100644 --- a/frontend/utils/editTestimonial.ts +++ b/frontend/components/software/edit/testimonials/apiSoftwareTestimonial.ts @@ -1,28 +1,35 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 -import {createJsonHeaders, extractReturnMessage} from './fetchHelpers' -import {NewTestimonial, Testimonial} from '../types/Testimonial' -import logger from './logger' +import logger from '~/utils/logger' +import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' +import {NewTestimonial, Testimonial} from '~/types/Testimonial' +export type NewSoftwareTestimonial = NewTestimonial & { + software: string +} + +export type SoftwareTestimonial = NewSoftwareTestimonial & { + id: string + position: number +} -export async function getTestimonialsForSoftware({software, frontend, token}: - {software: string, frontend?: boolean, token?: string}) { +export async function getTestimonialsForSoftware({software, token}: + {software: string, token?: string}) { try { - let url = `${process.env.POSTGREST_URL}/testimonial?software=eq.${software}&order=position.asc` - if (frontend === true) { - url = `/api/v1/testimonial?software=eq.${software}&order=position.asc` - } + let url = `${getBaseUrl()}/testimonial?software=eq.${software}&order=position.asc` const resp = await fetch(url, { method: 'GET', headers: createJsonHeaders(token) }) if (resp.status === 200) { - const data: Testimonial[] = await resp.json() + const data: SoftwareTestimonial[] = await resp.json() // update position to reflect array return data.map((item, pos) => { return { @@ -41,9 +48,9 @@ export async function getTestimonialsForSoftware({software, frontend, token}: } } -export async function postTestimonial({testimonial, token}: { testimonial: NewTestimonial, token: string }) { +export async function postTestimonial({testimonial, token}: { testimonial: NewSoftwareTestimonial, token: string }) { try { - const url = '/api/v1/testimonial' + const url = `${getBaseUrl()}/testimonial` const resp = await fetch(url, { method: 'POST', headers: { @@ -80,9 +87,9 @@ export async function postTestimonial({testimonial, token}: { testimonial: NewTe } -export async function patchTestimonial({testimonial, token}: { testimonial: Testimonial, token: string }) { +export async function patchTestimonial({testimonial, token}: { testimonial: SoftwareTestimonial, token: string }) { try { - const url = `/api/v1/testimonial?id=eq.${testimonial.id}` + const url = `${getBaseUrl()}/testimonial?id=eq.${testimonial.id}` const resp = await fetch(url, { method: 'PATCH', headers: { @@ -102,9 +109,11 @@ export async function patchTestimonial({testimonial, token}: { testimonial: Test export async function patchTestimonialPositions({testimonials, token}: { testimonials: Testimonial[], token: string }) { try { + // if the array is empty return + if (testimonials.length === 0) return {status:200,message:'OK'} // create all requests const requests = testimonials.map(testimonial => { - const url = `/api/v1/testimonial?id=eq.${testimonial.id}` + const url = `${getBaseUrl()}/testimonial?id=eq.${testimonial.id}` return fetch(url, { method: 'PATCH', headers: { @@ -132,7 +141,7 @@ export async function patchTestimonialPositions({testimonials, token}: { testimo export async function deleteTestimonialById({id, token}: { id: string, token: string }) { try { - const url = `/api/v1/testimonial?id=eq.${id}` + const url = `${getBaseUrl()}/testimonial?id=eq.${id}` const resp = await fetch(url, { method: 'DELETE', headers: { diff --git a/frontend/components/software/edit/testimonials/index.tsx b/frontend/components/software/edit/testimonials/index.tsx index bbebb0c17..fd1fbb086 100644 --- a/frontend/components/software/edit/testimonials/index.tsx +++ b/frontend/components/software/edit/testimonials/index.tsx @@ -10,34 +10,28 @@ import {useState} from 'react' import AddIcon from '@mui/icons-material/Add' import Button from '@mui/material/Button' -import {useSession} from '~/auth' -import useSnackbar from '../../../snackbar/useSnackbar' -import {NewTestimonial, Testimonial} from '../../../../types/Testimonial' -import { - postTestimonial, patchTestimonial, - deleteTestimonialById, patchTestimonialPositions -} from '../../../../utils/editTestimonial' -import {sortOnNumProp} from '../../../../utils/sortFn' -import ContentLoader from '../../../layout/ContentLoader' -import ConfirmDeleteModal from '../../../layout/ConfirmDeleteModal' - -import EditTestimonialModal from './EditTestimonialModal' -import EditSection from '../../../layout/EditSection' -import EditSectionTitle from '../../../layout/EditSectionTitle' +import {NewTestimonial, Testimonial} from '~/types/Testimonial' +import ContentLoader from '~/components/layout/ContentLoader' +import ConfirmDeleteModal from '~/components/layout/ConfirmDeleteModal' +import EditSection from '~/components/layout/EditSection' +import EditSectionTitle from '~/components/layout/EditSectionTitle' +import {testimonialInformation as config} from '../editSoftwareConfig' import {ModalProps,ModalStates} from '../editSoftwareTypes' - +import EditTestimonialModal from './EditTestimonialModal' import SortableTestimonialList from './SortableTestimonialList' -import useTestimonals from './useTestimonials' - +import useTestimonals from './useSoftwareTestimonials' type EditTestimonialModal = ModalProps & { testimonial?: NewTestimonial | Testimonial } export default function SoftwareTestimonials() { - const {token} = useSession() - const {showErrorMessage, showSuccessMessage} = useSnackbar() - const {loading,software,testimonials,setTestimonials} = useTestimonals() + const { + loading,software,testimonials, + addTestimonial,updateTestimonial, + sortedTestimonials,deleteTestimonial + } = useTestimonals() + const [modal, setModal] = useState>({ edit: { open: false, @@ -57,26 +51,6 @@ export default function SoftwareTestimonials() { // console.log("loading...", loading) // console.groupEnd() - function updateTestimonialList({data, pos}: { data: Testimonial, pos?: number }) { - if (typeof pos == 'number') { - // REPLACE existing item and sort - const list = [ - ...testimonials.slice(0, pos), - data, - ...testimonials.slice(pos+1) - ].sort((a,b)=>sortOnNumProp(a,b,'position')) - // pass new list with addition contributor - setTestimonials(list) - } else { - // ADD item and sort - const list = [ - ...testimonials, - data - ].sort((a,b)=>sortOnNumProp(a,b,'position')) - setTestimonials(list) - } - } - function loadTestimonialIntoModal(testimonial:NewTestimonial|Testimonial,pos?:number) { setModal({ edit: { @@ -105,7 +79,6 @@ export default function SoftwareTestimonials() { // console.log('Add new testimonial') loadTestimonialIntoModal({ id: null, - software: software?.id ?? '', message: null, source: null, position: testimonials.length + 1 @@ -123,29 +96,13 @@ export default function SoftwareTestimonials() { } async function onSubmitTestimonial({data,pos}:{data:Testimonial|NewTestimonial,pos?:number}) { - // debugger closeModals() - // if id present we update - if (data?.id) { - const resp = await patchTestimonial({testimonial: data as Testimonial, token}) - // debugger - if (resp.status === 200) { - updateTestimonialList({data:data as Testimonial,pos}) - } else { - showErrorMessage(`Failed to update testimonial. Error: ${resp.message}`) - } - } else { + if (data?.id === null) { // new testimonial - const resp = await postTestimonial({testimonial: data as NewTestimonial, token}) - // debugger - if (resp.status === 201) { - // we receive processed item as message - const record = resp.message - updateTestimonialList({data: record, pos}) - showSuccessMessage(`Created new testimonial for ${software.brand_name}!`) - } else { - showErrorMessage(`Failed to add testimonial. Error: ${resp.message}`) - } + addTestimonial(data) + } else if (typeof pos === 'number') { + // if id present we update + updateTestimonial({data: data as Testimonial, pos}) } } @@ -163,53 +120,6 @@ export default function SoftwareTestimonials() { }) } - async function deleteTestimonial(pos?:number) { - if (typeof pos=='undefined') return - closeModals() - // debugger - const testimonial = testimonials[pos] - if (testimonial?.id) { - const resp = await deleteTestimonialById({id: testimonial?.id ?? '', token}) - // console.log('deleteTestimonial...resp...', resp) - // debugger - if (resp.status === 200) { - // remove item from the list - const list = [ - ...testimonials.slice(0, pos), - ...testimonials.slice(pos+1) - ].map((item,pos) => { - item.position = pos + 1 - return item - }) - // patch testimonials - await sortedTestimonials(list) - } else { - showErrorMessage(`Failed to remove testimonial! Error: ${resp.message}`) - } - } - } - - async function sortedTestimonials(newList: Testimonial[]) { - if (newList.length > 0) { - // update ui first - setTestimonials(newList) - // update db - const resp = await patchTestimonialPositions({ - testimonials: newList, - token - }) - if (resp.status !== 200) { - // revert back - setTestimonials(testimonials) - // show error - showErrorMessage(`Failed to update testimonial positions! Error: ${resp.message}`) - } - } else { - // reset list - setTestimonials([]) - } - } - function getTestimonialSubtitle() { if (testimonials?.length === 1) { return `${software?.brand_name} has 1 testimonial` @@ -242,22 +152,37 @@ export default function SoftwareTestimonials() { /> - - Are you sure you want to remove testimonial from source {modal.delete.displayName ?? ''}?

- } - onCancel={closeModals} - onDelete={()=>deleteTestimonial(modal.delete.pos)} - /> + + { + modal.edit.open ? + + : null + } + + {modal.delete.open ? + Are you sure you want to remove testimonial from source {modal.delete.displayName ?? ''}?

+ } + onCancel={closeModals} + onDelete={()=> { + if (typeof modal.delete.pos === 'number'){ + deleteTestimonial(modal.delete.pos) + } + closeModals() + }} + /> + : null + } ) } diff --git a/frontend/components/software/edit/testimonials/useSoftwareTestimonials.tsx b/frontend/components/software/edit/testimonials/useSoftwareTestimonials.tsx new file mode 100644 index 000000000..88da5c316 --- /dev/null +++ b/frontend/components/software/edit/testimonials/useSoftwareTestimonials.tsx @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useCallback, useEffect, useState} from 'react' + +import {useSession} from '~/auth' +import {sortOnNumProp} from '~/utils/sortFn' +import {NewTestimonial, Testimonial} from '~/types/Testimonial' +import useSnackbar from '~/components/snackbar/useSnackbar' +import useSoftwareContext from '../useSoftwareContext' +import { + deleteTestimonialById, getTestimonialsForSoftware, + patchTestimonial, patchTestimonialPositions, + postTestimonial +} from './apiSoftwareTestimonial' + + +export default function useSoftwareTestimonals() { + const {token} = useSession() + const {software} = useSoftwareContext() + const {showErrorMessage} = useSnackbar() + const [testimonials, setTestimonials] = useState([]) + const [loading, setLoading] = useState(true) + const [loadedSoftware, setLoadedSoftware] = useState('') + + const addTestimonial = useCallback(async(item:NewTestimonial)=>{ + if (software.id && token){ + const testimonial = { + ...item, + software: software.id + } + const resp = await postTestimonial({ + testimonial, + token + }) + // debugger + if (resp.status === 201) { + setTestimonials((data)=>{ + return [ + ...data, + resp.message + ] + }) + } else { + showErrorMessage(`Failed to add testimonial. Error: ${resp.message}`) + } + } + // ignore showErrorMessage as dependency + // eslint-disable-next-line react-hooks/exhaustive-deps + },[software.id,token]) + + const updateTestimonial = useCallback(async({data, pos}: { data: Testimonial, pos: number })=>{ + if (typeof pos == 'number' && data.id) { + const testimonial = { + ...data, + software: software.id + } + const resp = await patchTestimonial({testimonial, token}) + if (resp.status === 200) { + setTestimonials((testimonials)=>{ + // replace item in state + const list = [ + ...testimonials.slice(0, pos), + testimonial, + ...testimonials.slice(pos+1) + ].sort((a,b)=>sortOnNumProp(a,b,'position')) + return list + }) + } else { + showErrorMessage(`Failed to update testimonial. Error: ${resp.message}`) + } + } + // ignore showErrorMessage as dependency + // eslint-disable-next-line react-hooks/exhaustive-deps + },[software.id,token]) + + const deleteTestimonial = useCallback(async(pos:number)=>{ + const id = testimonials[pos].id + if (id){ + const resp = await deleteTestimonialById({id, token}) + if (resp.status === 200) { + // remove item from the list + const list = testimonials + .filter(item=>item.id!==id) + .map((item,pos) => { + item.position = pos + 1 + return item + }) + // patch testimonials position + await sortedTestimonials(list) + } else { + showErrorMessage(`Failed to remove testimonial! Error: ${resp.message}`) + } + } + // ignore showErrorMessage as dependency + // eslint-disable-next-line react-hooks/exhaustive-deps + },[token,testimonials]) + + const sortedTestimonials = useCallback(async(newList:Testimonial[])=>{ + // update ui first + setTestimonials(newList) + // update db + const resp = await patchTestimonialPositions({ + testimonials: newList, + token + }) + if (resp.status !== 200) { + // revert back + setTestimonials(testimonials) + // show error + showErrorMessage(`Failed to update testimonial positions! Error: ${resp.message}`) + } + // ignore showErrorMessage as dependency + // eslint-disable-next-line react-hooks/exhaustive-deps + },[token,testimonials]) + + useEffect(() => { + let abort = false + const getTestimonials = async (software:string,token:string) => { + const resp = await getTestimonialsForSoftware({ + software, + token + }) + if (abort) return + // update state + setTestimonials(resp ?? []) + setLoadedSoftware(software) + setLoading(false) + } + if (software?.id && token && + software?.id !== loadedSoftware) { + getTestimonials(software.id,token) + } + return () => { abort = true } + },[software?.id,token,loadedSoftware]) + + return { + loading, + testimonials, + software, + addTestimonial, + updateTestimonial, + sortedTestimonials, + deleteTestimonial + } +} diff --git a/frontend/components/software/edit/testimonials/useTestimonials.tsx b/frontend/components/software/edit/testimonials/useTestimonials.tsx deleted file mode 100644 index 0fe8e8e2a..000000000 --- a/frontend/components/software/edit/testimonials/useTestimonials.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 dv4all -// -// SPDX-License-Identifier: Apache-2.0 - -import {useEffect, useState} from 'react' -import {useSession} from '~/auth' -import {Testimonial} from '~/types/Testimonial' -import {getTestimonialsForSoftware} from '~/utils/editTestimonial' -import useSoftwareContext from '../useSoftwareContext' - -export default function useTestimonals() { - const {token} = useSession() - const {software} = useSoftwareContext() - const [testimonials, setTestimonials] = useState([]) - const [loading, setLoading] = useState(true) - const [loadedSoftware, setLoadedSoftware] = useState('') - - useEffect(() => { - let abort = false - const getTestimonials = async (software:string,token:string) => { - const resp = await getTestimonialsForSoftware({ - software, - token, - frontend:true - }) - if (abort) return - // update state - setTestimonials(resp ?? []) - setLoadedSoftware(software) - setLoading(false) - } - if (software?.id && token && - software?.id !== loadedSoftware) { - getTestimonials(software.id,token) - } - return () => { abort = true } - },[software?.id,token,loadedSoftware]) - - return { - loading, - testimonials, - setTestimonials, - software - } -} diff --git a/frontend/pages/projects/[slug]/index.tsx b/frontend/pages/projects/[slug]/index.tsx index d4cdef42b..1e834d750 100644 --- a/frontend/pages/projects/[slug]/index.tsx +++ b/frontend/pages/projects/[slug]/index.tsx @@ -26,6 +26,7 @@ import {MentionItemProps} from '~/types/Mention' import {Person} from '~/types/Contributor' import {ProjectOrganisationProps} from '~/types/Organisation' import {SoftwareOverviewItemProps} from '~/types/SoftwareTypes' +import {Testimonial} from '~/types/Testimonial' import AppHeader from '~/components/AppHeader' import AppFooter from '~/components/AppFooter' import EditPageButton from '~/components/layout/EditPageButton' @@ -42,6 +43,8 @@ import RelatedSoftwareSection from '~/components/software/RelatedSoftwareSection import ProjectInfo from '~/components/projects/ProjectInfo' import RelatedProjectsSection from '~/components/projects/RelatedProjectsSection' import MentionsSection from '~/components/mention/MentionsSection' +import {getTestimonialsForProject} from '~/components/projects/edit/testimonials/apiProjectTestimonial' +import TestimonialSection from '~/components/software/TestimonialsSection' export interface ProjectPageProps extends ScriptProps{ slug: string @@ -53,6 +56,7 @@ export interface ProjectPageProps extends ScriptProps{ links: ProjectLink[], output: MentionItemProps[], impact: MentionItemProps[], + testimonials: Testimonial[] team: Person[], relatedSoftware: SoftwareOverviewItemProps[], relatedProjects: RelatedProject[] @@ -61,13 +65,13 @@ export interface ProjectPageProps extends ScriptProps{ export default function ProjectPage(props: ProjectPageProps) { const {slug, project, isMaintainer, organisations, researchDomains, keywords, links, output, impact, team, - relatedSoftware, relatedProjects + relatedSoftware, relatedProjects, testimonials } = props if (!project?.title){ return } - // console.log('ProjectPage...output...', output) + // console.log('ProjectPage...testimonials...', testimonials) return ( <> {/* Page Head meta tags */} @@ -124,6 +128,10 @@ export default function ProjectPage(props: ProjectPageProps) { mentions={output} /> + {/* Testimonials (uses software components) */} + {/* Team (uses software components) */}