Skip to content

Commit

Permalink
♻️ Refactor API Calls
Browse files Browse the repository at this point in the history
  • Loading branch information
0x46616c6b committed Feb 2, 2025
1 parent c3cb294 commit 7209051
Show file tree
Hide file tree
Showing 19 changed files with 358 additions and 124 deletions.
38 changes: 37 additions & 1 deletion src/api/Api.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { apiClient, apiHeaders } from './Api'
import { apiClient, apiHeaders, ApiResponse, handleApiCall } from './Api'

describe('apiClient', () => {
beforeEach(() => {
Expand Down Expand Up @@ -31,3 +31,39 @@ describe('apiClient', () => {
expect(fetchMock).toHaveBeenCalledWith('/admin/features', { headers: apiHeaders('token') })
})
})

describe('handleApiCall', async () => {
const onSuccess = vi.fn()
const onError = vi.fn()
const onFailure = vi.fn()

beforeEach(() => {
onSuccess.mockClear()
onError.mockClear()
onFailure.mockClear()
})

it('should call onSuccess on success', async () => {
const apiCall = Promise.resolve({ status: 'success' } as ApiResponse<unknown>)
await handleApiCall(apiCall, { onSuccess, onError, onFailure })
expect(onSuccess).toHaveBeenCalledTimes(1)
expect(onError).not.toHaveBeenCalled()
expect(onFailure).not.toHaveBeenCalled()
})

it('should call onError on error', async () => {
const apiCall = Promise.resolve({ status: 'error' } as ApiResponse<unknown>)
await handleApiCall(apiCall, { onSuccess, onError, onFailure })
expect(onSuccess).not.toHaveBeenCalled()
expect(onError).toHaveBeenCalledTimes(1)
expect(onFailure).not.toHaveBeenCalled()
})

it('should call onFailure on failure', async () => {
const apiCall = Promise.reject(new Error('Network error'))
await handleApiCall(apiCall, { onSuccess, onError, onFailure })
expect(onSuccess).not.toHaveBeenCalled()
expect(onError).not.toHaveBeenCalled()
expect(onFailure).toHaveBeenCalledTimes(1)
})
})
21 changes: 21 additions & 0 deletions src/api/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,24 @@ export function apiHeaders(token: string): HeadersInit {
'Content-Type': 'application/json',
}
}

interface ApiCallParams<T> {
onSuccess: (response: ApiResponse<T>) => void
onError: (response: ApiResponse<T>) => void
onFailure: (error: unknown) => void
}

export async function handleApiCall<T>(apiCall: Promise<ApiResponse<T>>, params: ApiCallParams<T>): Promise<void> {
const { onSuccess, onError, onFailure } = params

try {
const response = await apiCall
if (response.status === 'error') {
onError(response)
} else {
onSuccess(response)
}
} catch (error) {
onFailure(error)
}
}
39 changes: 30 additions & 9 deletions src/components/ticker/BlueskyCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,55 @@ import { faGear, faPause, faPlay, faTrash } from '@fortawesome/free-solid-svg-ic
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Box, Button, Card, CardActions, CardContent, Divider, Link, Stack, Typography } from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import { FC, useCallback, useState } from 'react'
import { FC, useState } from 'react'
import { handleApiCall } from '../../api/Api'
import { Ticker, deleteTickerBlueskyApi, putTickerBlueskyApi } from '../../api/Ticker'
import useAuth from '../../contexts/useAuth'
import useNotification from '../../contexts/useNotification'
import BlueskyModalForm from './BlueskyModalForm'

interface Props {
ticker: Ticker
}

const BlueskyCard: FC<Props> = ({ ticker }) => {
const { createNotification } = useNotification()
const { token } = useAuth()
const [open, setOpen] = useState<boolean>(false)

const queryClient = useQueryClient()

const bluesky = ticker.bluesky

const handleDelete = useCallback(() => {
deleteTickerBlueskyApi(token, ticker).finally(() => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
const handleDelete = () => {
handleApiCall(deleteTickerBlueskyApi(token, ticker), {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
createNotification({ content: 'Bluesky integration successfully deleted', severity: 'success' })
},
onError: () => {
createNotification({ content: 'Failed to delete Bluesky integration', severity: 'error' })
},
onFailure: error => {
createNotification({ content: error as string, severity: 'error' })
},
})
}, [token, queryClient, ticker])
}

const handleToggle = useCallback(() => {
putTickerBlueskyApi(token, { active: !bluesky.active }, ticker).finally(() => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
const handleToggle = () => {
handleApiCall(putTickerBlueskyApi(token, { active: !bluesky.active }, ticker), {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
createNotification({ content: `Bluesky integration ${bluesky.active ? 'disabled' : 'enabled'} successfully`, severity: 'success' })
},
onError: () => {
createNotification({ content: 'Failed to update Bluesky integration', severity: 'error' })
},
onFailure: error => {
createNotification({ content: error as string, severity: 'error' })
},
})
}, [bluesky.active, token, queryClient, ticker])
}

const profileLink = (
<Link href={'https://bsky.app/profile/' + bluesky.handle} rel="noreferrer" target="_blank">
Expand Down
17 changes: 11 additions & 6 deletions src/components/ticker/BlueskyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Grid from '@mui/material/Grid2'
import { useQueryClient } from '@tanstack/react-query'
import { FC } from 'react'
import { useForm } from 'react-hook-form'
import { handleApiCall } from '../../api/Api'
import { Ticker, TickerBlueskyFormData, putTickerBlueskyApi } from '../../api/Ticker'
import useAuth from '../../contexts/useAuth'
import useNotification from '../../contexts/useNotification'
Expand Down Expand Up @@ -31,15 +32,19 @@ const BlueskyForm: FC<Props> = ({ callback, ticker }) => {
const queryClient = useQueryClient()

const onSubmit = handleSubmit(data => {
putTickerBlueskyApi(token, data, ticker).then(response => {
if (response.status == 'error') {
setError('root.authenticationFailed', { message: 'Authentication failed' })
createNotification({ content: 'Bluesky integration failed to update', severity: 'error' })
} else {
handleApiCall(putTickerBlueskyApi(token, data, ticker), {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
createNotification({ content: 'Bluesky integration was successfully updated', severity: 'success' })
callback()
}
},
onError: () => {
setError('root.authenticationFailed', { message: 'Authentication failed' })
createNotification({ content: 'Failed to update Bluesky integration', severity: 'error' })
},
onFailure: error => {
createNotification({ content: error as string, severity: 'error' })
},
})
})

Expand Down
39 changes: 30 additions & 9 deletions src/components/ticker/MastodonCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,55 @@ import { faGear, faPause, faPlay, faTrash } from '@fortawesome/free-solid-svg-ic
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Box, Button, Card, CardActions, CardContent, Divider, Link, Stack, Typography } from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import { FC, useCallback, useState } from 'react'
import { FC, useState } from 'react'
import { handleApiCall } from '../../api/Api'
import { Ticker, deleteTickerMastodonApi, putTickerMastodonApi } from '../../api/Ticker'
import useAuth from '../../contexts/useAuth'
import useNotification from '../../contexts/useNotification'
import MastodonModalForm from './MastodonModalForm'

interface Props {
ticker: Ticker
}

const MastodonCard: FC<Props> = ({ ticker }) => {
const { createNotification } = useNotification()
const { token } = useAuth()
const [open, setOpen] = useState<boolean>(false)

const queryClient = useQueryClient()

const mastodon = ticker.mastodon

const handleDelete = useCallback(() => {
deleteTickerMastodonApi(token, ticker).finally(() => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
const handleDelete = () => {
handleApiCall(deleteTickerMastodonApi(token, ticker), {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
createNotification({ content: 'Mastodon integration successfully deleted', severity: 'success' })
},
onError: () => {
createNotification({ content: 'Failed to delete Mastodon integration', severity: 'error' })
},
onFailure: error => {
createNotification({ content: error as string, severity: 'error' })
},
})
}, [token, queryClient, ticker])
}

const handleToggle = useCallback(() => {
putTickerMastodonApi(token, { active: !mastodon.active }, ticker).finally(() => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
const handleToggle = () => {
handleApiCall(putTickerMastodonApi(token, { active: !mastodon.active }, ticker), {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
createNotification({ content: `Mastodon integration ${mastodon.active ? 'disabled' : 'enabled'} successfully`, severity: 'success' })
},
onError: () => {
createNotification({ content: 'Failed to update Mastodon integration', severity: 'error' })
},
onFailure: error => {
createNotification({ content: error as string, severity: 'error' })
},
})
}, [mastodon.active, token, queryClient, ticker])
}

const profileLink = (
<Link href={mastodon.server + '/web/@' + mastodon.name} rel="noreferrer" target="_blank">
Expand Down
17 changes: 13 additions & 4 deletions src/components/ticker/MastodonForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Grid from '@mui/material/Grid2'
import { useQueryClient } from '@tanstack/react-query'
import { FC } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { handleApiCall } from '../../api/Api'
import { Ticker, TickerMastodonFormData, putTickerMastodonApi } from '../../api/Ticker'
import useAuth from '../../contexts/useAuth'
import useNotification from '../../contexts/useNotification'
Expand All @@ -25,10 +26,18 @@ const MastodonForm: FC<Props> = ({ callback, ticker }) => {
const queryClient = useQueryClient()

const onSubmit: SubmitHandler<TickerMastodonFormData> = data => {
putTickerMastodonApi(token, data, ticker).finally(() => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
createNotification({ content: 'Mastodon integration was successfully updated', severity: 'success' })
callback()
handleApiCall(putTickerMastodonApi(token, data, ticker), {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
createNotification({ content: 'Mastodon integration was successfully updated', severity: 'success' })
callback()
},
onError: () => {
createNotification({ content: 'Failed to update Mastodon integration', severity: 'error' })
},
onFailure: error => {
createNotification({ content: error as string, severity: 'error' })
},
})
}

Expand Down
30 changes: 17 additions & 13 deletions src/components/ticker/SignalGroupAdminForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Alert, FormGroup, InputAdornment, TextField } from '@mui/material'
import Grid from '@mui/material/Grid2'
import { FC } from 'react'
import { useForm } from 'react-hook-form'
import { handleApiCall } from '../../api/Api'
import { Ticker, TickerSignalGroupAdminFormData, putTickerSignalGroupAdminApi } from '../../api/Ticker'
import useAuth from '../../contexts/useAuth'
import useNotification from '../../contexts/useNotification'
Expand All @@ -26,19 +27,22 @@ const SignalGroupAdminForm: FC<Props> = ({ callback, ticker, setSubmitting }) =>

const onSubmit = handleSubmit(data => {
setSubmitting(true)
putTickerSignalGroupAdminApi(token, data, ticker)
.then(response => {
if (response.status == 'error') {
createNotification({ content: 'Failed to add number to Signal group', severity: 'error' })
setError('number', { message: 'Failed to add number to Signal group' })
} else {
createNotification({ content: 'Number successfully added to Signal group', severity: 'success' })
callback()
}
})
.finally(() => {
setSubmitting(false)
})

handleApiCall(putTickerSignalGroupAdminApi(token, data, ticker), {
onSuccess: () => {
createNotification({ content: 'Number successfully added to Signal group', severity: 'success' })
callback()
},
onError: () => {
createNotification({ content: 'Failed to add number to Signal group', severity: 'error' })
setError('number', { message: 'Failed to add number to Signal group' })
},
onFailure: error => {
createNotification({ content: error as string, severity: 'error' })
},
})

setSubmitting(false)
})

return (
Expand Down
42 changes: 30 additions & 12 deletions src/components/ticker/SignalGroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
Typography,
} from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import { FC, useCallback, useState } from 'react'
import { FC, useState } from 'react'
import { handleApiCall } from '../../api/Api'
import { Ticker, deleteTickerSignalGroupApi, putTickerSignalGroupApi } from '../../api/Ticker'
import useAuth from '../../contexts/useAuth'
import useNotification from '../../contexts/useNotification'
Expand All @@ -44,24 +45,41 @@ const SignalGroupCard: FC<Props> = ({ ticker }) => {

const handleAdd = () => {
setSubmittingAdd(true)
putTickerSignalGroupApi(token, { active: true }, ticker)
.finally(() => {

handleApiCall(putTickerSignalGroupApi(token, { active: true }, ticker), {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
createNotification({ content: 'Signal Group enabled successfully', severity: 'success' })
setSubmittingAdd(false)
})
.catch(() => {
},
onError: () => {
createNotification({ content: 'Failed to configure Signal group', severity: 'error' })
})
},
onFailure: error => {
createNotification({ content: error as string, severity: 'error' })
},
})

setSubmittingAdd(false)
}

const handleToggle = useCallback(() => {
const handleToggle = () => {
setSubmittingToggle(true)
putTickerSignalGroupApi(token, { active: !signalGroup.active }, ticker).finally(() => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
setSubmittingToggle(false)

handleApiCall(putTickerSignalGroupApi(token, { active: !signalGroup.active }, ticker), {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] })
createNotification({ content: `Signal group ${signalGroup.active ? 'disabled' : 'enabled'} successfully`, severity: 'success' })
},
onError: () => {
createNotification({ content: 'Failed to update Signal group', severity: 'error' })
},
onFailure: error => {
createNotification({ content: error as string, severity: 'error' })
},
})
}, [token, queryClient, signalGroup.active, ticker])

setSubmittingToggle(false)
}

const handleDelete = () => {
setSubmittingDelete(true)
Expand Down
Loading

0 comments on commit 7209051

Please sign in to comment.