From 46f615a6e21018811bda288f448f0971ca4dbac5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com>
Date: Mon, 8 Apr 2024 09:51:03 +0200
Subject: [PATCH] feat: add publications list modal download partial

---
 .../PublicationsModalLayout.component.tsx     | 29 ++++++++--
 .../utils/getBasePublications.ts              | 32 +++++++++++
 .../utils/getStandarizedPublications.ts       | 30 +++++++++++
 .../utils/useDownloadPublicationsAsCSVFile.ts | 54 +++++++++++++++++++
 src/types/publications.ts                     |  9 ++++
 5 files changed, 149 insertions(+), 5 deletions(-)
 create mode 100644 src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/getBasePublications.ts
 create mode 100644 src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/getStandarizedPublications.ts
 create mode 100644 src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/useDownloadPublicationsAsCSVFile.ts
 create mode 100644 src/types/publications.ts

diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.component.tsx b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.component.tsx
index 8d92fe19..9ab85767 100644
--- a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.component.tsx
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/PublicationsModalLayout.component.tsx
@@ -1,11 +1,15 @@
-import { twMerge } from 'tailwind-merge';
+import spinnerIcon from '@/assets/vectors/icons/spinner.svg';
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { closeModal } from '@/redux/modal/modal.slice';
-import { Icon } from '@/shared/Icon';
 import { filteredSizeSelector } from '@/redux/publications/publications.selectors';
-import { MODAL_ROLE } from './PublicationsModalLayout.constants';
+import { Button } from '@/shared/Button';
+import { Icon } from '@/shared/Icon';
+import Image from 'next/image';
+import { twMerge } from 'tailwind-merge';
 import { PublicationsSearch } from '../PublicationsSearch';
+import { MODAL_ROLE } from './PublicationsModalLayout.constants';
+import { useDownloadPublicationsAsCSVFile } from './utils/useDownloadPublicationsAsCSVFile';
 
 type ModalLayoutProps = {
   children: React.ReactNode;
@@ -14,6 +18,7 @@ type ModalLayoutProps = {
 export const PublicationsModalLayout = ({ children }: ModalLayoutProps): JSX.Element => {
   const dispatch = useAppDispatch();
   const numberOfPublications = useAppSelector(filteredSizeSelector);
+  const { downloadPublicationsAsCSVFile, isLoading } = useDownloadPublicationsAsCSVFile();
 
   const handleCloseModal = (): void => {
     dispatch(closeModal());
@@ -27,8 +32,22 @@ export const PublicationsModalLayout = ({ children }: ModalLayoutProps): JSX.Ele
       <div className="flex h-full w-full items-center justify-center">
         <div className={twMerge('flex h-5/6 w-10/12	flex-col	overflow-hidden rounded-lg')}>
           <div className="flex items-center  justify-between bg-white p-[24px] text-xl">
-            <div className="font-semibold">
-              <div>Publications ({numberOfPublications} results)</div>
+            <div className="flex items-center gap-4">
+              <div className="font-semibold">
+                <div>Publications ({numberOfPublications} results)</div>
+              </div>
+              <Button onClick={downloadPublicationsAsCSVFile} disabled={isLoading}>
+                {isLoading && (
+                  <Image
+                    src={spinnerIcon}
+                    alt="spinner icon"
+                    height={12}
+                    width={12}
+                    className="animate-spin"
+                  />
+                )}
+                Download CSV
+              </Button>
             </div>
             <div className="flex flex-row flex-nowrap items-center">
               <PublicationsSearch />
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/getBasePublications.ts b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/getBasePublications.ts
new file mode 100644
index 00000000..7725df11
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/getBasePublications.ts
@@ -0,0 +1,32 @@
+import { publicationsResponseSchema } from '@/models/publicationsResponseSchema';
+import { apiPath } from '@/redux/apiPath';
+import { PUBLICATIONS_FETCHING_ERROR_PREFIX } from '@/redux/publications/publications.constatns';
+import { axiosInstance } from '@/services/api/utils/axiosInstance';
+import { Publication, PublicationsResponse } from '@/types/models';
+import { getErrorMessage } from '@/utils/getErrorMessage';
+import { showToast } from '@/utils/showToast';
+import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
+
+interface Args {
+  length: number;
+}
+
+export const getBasePublications = async ({ length }: Args): Promise<Publication[]> => {
+  try {
+    const response = await axiosInstance.get<PublicationsResponse>(
+      apiPath.getPublications({ params: { length } }),
+    );
+
+    const isDataValid = validateDataUsingZodSchema(response.data, publicationsResponseSchema);
+
+    return isDataValid ? response.data.data : [];
+  } catch (error) {
+    const errorMessage = getErrorMessage({ error, prefix: PUBLICATIONS_FETCHING_ERROR_PREFIX });
+    showToast({
+      type: 'error',
+      message: errorMessage,
+    });
+
+    return [];
+  }
+};
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/getStandarizedPublications.ts b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/getStandarizedPublications.ts
new file mode 100644
index 00000000..061729de
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/getStandarizedPublications.ts
@@ -0,0 +1,30 @@
+import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
+import { Publication } from '@/types/models';
+import { StandarizedPublication } from '@/types/publications';
+
+interface Options {
+  modelNameIdMap: Record<number, string>;
+}
+
+export const getStandarizedPublication = (
+  publication: Publication,
+  options: Options,
+): StandarizedPublication => {
+  const {
+    publication: { article },
+    elements,
+  } = publication;
+  const { modelNameIdMap } = options;
+  const { title, authors, journal, year, pubmedId } = article;
+  const mainElement = elements?.[FIRST_ARRAY_ELEMENT];
+
+  return {
+    pubmedId,
+    journal,
+    title,
+    year: year ? `${year}` : '',
+    authors: authors.join(','),
+    modelName: mainElement ? modelNameIdMap[mainElement.modelId] : '',
+    elementId: '', // TODO:
+  };
+};
diff --git a/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/useDownloadPublicationsAsCSVFile.ts b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/useDownloadPublicationsAsCSVFile.ts
new file mode 100644
index 00000000..306515c5
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/PublicationsModal/PublicationsModalLayout/utils/useDownloadPublicationsAsCSVFile.ts
@@ -0,0 +1,54 @@
+import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { modelsNameMapSelector } from '@/redux/models/models.selectors';
+import { StandarizedPublication } from '@/types/publications';
+import { useState } from 'react';
+import { getBasePublications } from './getBasePublications';
+import { getStandarizedPublication } from './getStandarizedPublications';
+
+export type DownloadPublicationsAsCSVFile = () => Promise<void>;
+
+interface UseDownloadPublicationsAsCSVFileResult {
+  downloadPublicationsAsCSVFile: DownloadPublicationsAsCSVFile;
+  isLoading: boolean;
+}
+
+export const useDownloadPublicationsAsCSVFile = (): UseDownloadPublicationsAsCSVFileResult => {
+  const [isLoading, setIsLoading] = useState<boolean>(false);
+  const [publications, setPublications] = useState<StandarizedPublication[]>([]);
+  const modelNameIdMap = useAppSelector(modelsNameMapSelector);
+
+  const getPublicationsAsList = async (): Promise<StandarizedPublication[]> => {
+    if (publications.length > SIZE_OF_EMPTY_ARRAY) {
+      return publications;
+    }
+
+    const basePublications = await getBasePublications({
+      length: 100, // TODO: change for number of all elements
+    });
+
+    const standarizedPublications = await Promise.all(
+      basePublications.map(async publication =>
+        getStandarizedPublication(publication, {
+          modelNameIdMap,
+        }),
+      ),
+    );
+
+    setPublications(standarizedPublications);
+    return standarizedPublications;
+  };
+
+  const downloadPublicationsAsCSVFile = async (): Promise<void> => {
+    setIsLoading(true);
+    const data = await getPublicationsAsList();
+    setIsLoading(false);
+
+    console.log({ data });
+  };
+
+  return {
+    downloadPublicationsAsCSVFile,
+    isLoading,
+  };
+};
diff --git a/src/types/publications.ts b/src/types/publications.ts
new file mode 100644
index 00000000..a9c9bfb3
--- /dev/null
+++ b/src/types/publications.ts
@@ -0,0 +1,9 @@
+export interface StandarizedPublication {
+  pubmedId: string;
+  year: string;
+  journal: string;
+  authors: string;
+  title: string;
+  modelName: string;
+  elementId: string;
+}
-- 
GitLab