diff --git a/package.json b/package.json index 1d85dde9a0..93aa0c9542 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "devDependencies": { "@babel/preset-typescript": "^7.23.3", "@next/bundle-analyzer": "^14.1.0", + "@tanstack/react-query-devtools": "^5.58.0", "@testing-library/cypress": "^10.0.1", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", diff --git a/src/apollo/apolloClient.ts b/src/apollo/apolloClient.ts index 6bf978265c..4cedd8970d 100644 --- a/src/apollo/apolloClient.ts +++ b/src/apollo/apolloClient.ts @@ -1,5 +1,10 @@ import { useMemo } from 'react'; -import { ApolloClient, InMemoryCache, ApolloLink } from '@apollo/client'; +import { + ApolloClient, + InMemoryCache, + ApolloLink, + NormalizedCacheObject, +} from '@apollo/client'; import { RetryLink } from '@apollo/client/link/retry'; import { setContext } from '@apollo/client/link/context'; import { onError } from '@apollo/client/link/error'; @@ -14,20 +19,21 @@ import { signOut } from '@/features/user/user.thunks'; import config from '@/configuration'; import { setShowSignWithWallet } from '@/features/modal/modal.slice'; -let apolloClient: any; +let apolloClient: ApolloClient | undefined; const ssrMode = isSSRMode; export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'; -const parseHeaders = (rawHeaders: any) => { +// Parses headers into the Headers object +const parseHeaders = (rawHeaders: string) => { const headers = new Headers(); // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space // https://tools.ietf.org/html/rfc7230#section-3.2 const preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' '); - preProcessedHeaders.split(/\r?\n/).forEach((line: any) => { + preProcessedHeaders.split(/\r?\n/).forEach((line: string) => { const parts = line.split(':'); - const key = parts.shift().trim(); + const key = parts.shift()?.trim(); if (key) { const value = parts.join(':').trim(); headers.append(key, value); @@ -36,6 +42,7 @@ const parseHeaders = (rawHeaders: any) => { return headers; }; +// Custom fetch logic with file upload handling const uploadFetch = (url: string, options: any) => new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); @@ -49,8 +56,11 @@ const uploadFetch = (url: string, options: any) => 'responseURL' in xhr ? xhr.responseURL : opts.headers.get('X-Request-URL'); + // TypeScript fix: Explicitly cast `xhr` to `XMLHttpRequest` to access responseText const body = - 'response' in xhr ? xhr.response : (xhr as any).responseText; + 'response' in xhr + ? xhr.response + : (xhr as XMLHttpRequest).responseText; resolve(new Response(body, opts)); }; xhr.onerror = () => { @@ -76,14 +86,16 @@ const uploadFetch = (url: string, options: any) => xhr.send(options.body); }); -const customFetch = (uri: any, options: any) => { +// Custom fetch function to determine when to use upload fetch or standard fetch +const customFetch = (uri: string, options: any) => { if (options.useUpload) { return uploadFetch(uri, options); } return fetch(uri, options); }; -function createApolloClient() { +// Creates the Apollo Client with the custom link setup +function createApolloClient(): ApolloClient { let userWalletAddress: string | null; if (!ssrMode) { userWalletAddress = localStorage.getItem(StorageLabel.USER); @@ -91,11 +103,13 @@ function createApolloClient() { const retryLink = new RetryLink(); + // Custom link for handling file uploads const httpLink = createUploadLink({ uri: config.BACKEND_LINK, fetch: customFetch as any, }); + // Auth link to add Authorization and locale headers const authLink = setContext((_, { headers }) => { let locale: string | null = !ssrMode ? localStorage.getItem(StorageLabel.LOCALE) @@ -117,12 +131,13 @@ function createApolloClient() { }; }); + // Error handling link const errorLink = onError(({ graphQLErrors, networkError, operation }) => { if (graphQLErrors) { console.log('operation', operation); graphQLErrors.forEach(err => { console.error('err', JSON.stringify(err)); - const { message, locations, path } = err; + const { message } = err; if (message.toLowerCase().includes('authentication required')) { console.log(Date.now(), 'sign out from graphQL'); // removes token and user from store @@ -190,7 +205,10 @@ function createApolloClient() { }); } -export function initializeApollo(initialState = null) { +// Initialize Apollo Client for SSR and client-side rendering +export function initializeApollo( + initialState: any = null, +): ApolloClient { const _apolloClient = apolloClient ?? createApolloClient(); // If your page has Next.js data fetching methods that use Apollo Client, the initial state @@ -202,7 +220,7 @@ export function initializeApollo(initialState = null) { // Merge the existing cache into data passed from getStaticProps/getServerSideProps const data = merge(initialState, existingCache, { // combine arrays using object equality (like in sets) - arrayMerge: (destinationArray, sourceArray) => [ + arrayMerge: (destinationArray: any[], sourceArray: any[]) => [ ...sourceArray, ...destinationArray.filter(d => sourceArray.every(s => !isEqual(d, s)), @@ -221,7 +239,11 @@ export function initializeApollo(initialState = null) { return _apolloClient; } -export function addApolloState(client: any, pageProps: any) { +// Adds Apollo Client's state to pageProps +export function addApolloState( + client: ApolloClient, + pageProps: any, +) { if (pageProps?.props) { pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract(); } @@ -229,10 +251,12 @@ export function addApolloState(client: any, pageProps: any) { return pageProps; } +// Custom React hook to use Apollo Client export function useApollo(pageProps: any) { const state = pageProps[APOLLO_STATE_PROP_NAME]; return useMemo(() => initializeApollo(state), [state]); } -export const client = initializeApollo(); +// Export the client instance +export const client: ApolloClient = initializeApollo(); diff --git a/src/apollo/gql/gqlProjects.ts b/src/apollo/gql/gqlProjects.ts index cc3d8a260b..6546c7d2c0 100644 --- a/src/apollo/gql/gqlProjects.ts +++ b/src/apollo/gql/gqlProjects.ts @@ -804,3 +804,9 @@ export const FETCH_RECURRING_DONATIONS_BY_DATE = gql` } } `; + +export const DELETE_DRAFT_PROJECT = gql` + mutation ($projectId: Float!) { + deleteDraftProject(projectId: $projectId) + } +`; diff --git a/src/components/views/project/projectDonations/ProjectRecurringDonationTable.tsx b/src/components/views/project/projectDonations/ProjectRecurringDonationTable.tsx index c2401362a1..087fa11487 100644 --- a/src/components/views/project/projectDonations/ProjectRecurringDonationTable.tsx +++ b/src/components/views/project/projectDonations/ProjectRecurringDonationTable.tsx @@ -31,7 +31,7 @@ import { ONE_MONTH_SECONDS } from '@/lib/constants/constants'; import ExternalLink from '@/components/ExternalLink'; import { ChainType } from '@/types/config'; import NetworkLogo from '@/components/NetworkLogo'; -import { EOrderBy } from '../../userProfile/UserProfile.view'; +import { EOrderBy } from '../../userProfile/projectsTab/type'; const itemPerPage = 10; diff --git a/src/components/views/userProfile/UserProfile.view.tsx b/src/components/views/userProfile/UserProfile.view.tsx index e84d4898b9..9235c4b532 100644 --- a/src/components/views/userProfile/UserProfile.view.tsx +++ b/src/components/views/userProfile/UserProfile.view.tsx @@ -29,7 +29,6 @@ import { isUserRegistered, shortenAddress, } from '@/lib/helpers'; -import { EDirection } from '@/apollo/types/gqlEnums'; import ExternalLink from '@/components/ExternalLink'; import IncompleteProfileToast from '@/components/views/userProfile/IncompleteProfileToast'; import { useAppDispatch, useAppSelector } from '@/features/hooks'; @@ -45,18 +44,6 @@ import { IGiverPFPToken } from '@/apollo/types/types'; import { useProfileContext } from '@/context/profile.context'; import { useGeneralWallet } from '@/providers/generalWalletProvider'; -export enum EOrderBy { - TokenAmount = 'TokenAmount', - UsdAmount = 'UsdAmount', - CreationDate = 'CreationDate', - Donations = 'Donations', -} - -export interface IOrder { - by: EOrderBy; - direction: EDirection; -} - export interface IUserProfileView {} const UserProfileView: FC = () => { diff --git a/src/components/views/userProfile/donationsTab/oneTimeTab/OneTimeDonationsTable.tsx b/src/components/views/userProfile/donationsTab/oneTimeTab/OneTimeDonationsTable.tsx index 5535169b73..0d60ef0e2e 100644 --- a/src/components/views/userProfile/donationsTab/oneTimeTab/OneTimeDonationsTable.tsx +++ b/src/components/views/userProfile/donationsTab/oneTimeTab/OneTimeDonationsTable.tsx @@ -13,10 +13,6 @@ import { smallFormatDate, formatTxLink } from '@/lib/helpers'; import { slugToProjectView } from '@/lib/routeCreators'; import ExternalLink from '@/components/ExternalLink'; import { IWalletDonation } from '@/apollo/types/types'; -import { - EOrderBy, - IOrder, -} from '@/components/views/userProfile/UserProfile.view'; import SortIcon from '@/components/SortIcon'; import DonationStatus from '@/components/badges/DonationStatusBadge'; import { @@ -27,6 +23,7 @@ import { import { Badge, EBadgeStatus } from '@/components/Badge'; import { formatDonation } from '@/helpers/number'; import NetworkLogo from '@/components/NetworkLogo'; +import { EOrderBy, IOrder } from '../../projectsTab/type'; interface OneTimeDonationTable { donations: IWalletDonation[]; diff --git a/src/components/views/userProfile/donationsTab/oneTimeTab/OneTimeTab.tsx b/src/components/views/userProfile/donationsTab/oneTimeTab/OneTimeTab.tsx index 4d5c713b58..7414084bd3 100644 --- a/src/components/views/userProfile/donationsTab/oneTimeTab/OneTimeTab.tsx +++ b/src/components/views/userProfile/donationsTab/oneTimeTab/OneTimeTab.tsx @@ -10,7 +10,7 @@ import { IWalletDonation } from '@/apollo/types/types'; import Pagination from '@/components/Pagination'; import NothingToSee from '@/components/views/userProfile/NothingToSee'; import DonationTable from '@/components/views/userProfile/donationsTab/oneTimeTab/OneTimeDonationsTable'; -import { IOrder, EOrderBy } from '../../UserProfile.view'; +import { EOrderBy, IOrder } from '../../projectsTab/type'; import { useProfileContext } from '@/context/profile.context'; import { WrappedSpinner } from '@/components/Spinner'; diff --git a/src/components/views/userProfile/projectsTab/DeleteProjectModal.tsx b/src/components/views/userProfile/projectsTab/DeleteProjectModal.tsx index edece33f41..04ffef2c7e 100644 --- a/src/components/views/userProfile/projectsTab/DeleteProjectModal.tsx +++ b/src/components/views/userProfile/projectsTab/DeleteProjectModal.tsx @@ -1,22 +1,55 @@ import styled from 'styled-components'; -import { type FC } from 'react'; +import { useState, type FC } from 'react'; import { Button, Flex, IconTrash32, P } from '@giveth/ui-design-system'; import { useIntl } from 'react-intl'; +import { useMutation } from '@tanstack/react-query'; import { IProject } from '@/apollo/types/types'; import { Modal } from '@/components/modals/Modal'; import { IModal } from '@/types/common'; import { useModalAnimation } from '@/hooks/useModalAnimation'; +import { client } from '@/apollo/apolloClient'; +import { DELETE_DRAFT_PROJECT } from '@/apollo/gql/gqlProjects'; +import { useAppDispatch } from '@/features/hooks'; +import { fetchUserByAddress } from '@/features/user/user.thunks'; +import { useGeneralWallet } from '@/providers/generalWalletProvider'; interface IDeleteProjectModal extends IModal { project: IProject; + refetchProjects: () => void; } const DeleteProjectModal: FC = ({ setShowModal, project, + refetchProjects, }) => { + const [isLoading, setIsLoading] = useState(false); const { formatMessage } = useIntl(); const { isAnimating, closeModal } = useModalAnimation(setShowModal); + const dispatch = useAppDispatch(); + const { walletAddress } = useGeneralWallet(); + + const { mutate: deleteProject, isPending } = useMutation({ + mutationFn: (projectId: number) => + client.mutate({ + mutation: DELETE_DRAFT_PROJECT, + variables: { projectId: projectId }, + }), + onSuccess: async () => { + setIsLoading(true); + await refetchProjects(); + walletAddress && + (await dispatch(fetchUserByAddress(walletAddress))); + setIsLoading(false); + closeModal(); + }, + }); + + const handleRemoveProject = async () => { + deleteProject(Number(project.id)); + }; + + const loading = isPending || isLoading; return ( = ({ id: 'component.delete_project.yes', })} size='small' - onClick={() => setShowModal(true)} + onClick={handleRemoveProject} + loading={loading} />