Skip to content

Commit

Permalink
✨ Extract Websites from Ticker to Integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
0x46616c6b committed Jan 26, 2025
1 parent 7d89e41 commit d706ba2
Show file tree
Hide file tree
Showing 19 changed files with 446 additions and 112 deletions.
88 changes: 88 additions & 0 deletions src/api/Ticker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
TickerFormData,
TickerMastodonFormData,
TickerTelegramFormData,
TickerWebsite,
deleteTickerApi,
deleteTickerBlueskyApi,
deleteTickerMastodonApi,
deleteTickerTelegramApi,
deleteTickerUserApi,
deleteTickerWebsitesApi,
fetchTickerApi,
fetchTickerUsersApi,
fetchTickersApi,
Expand All @@ -20,6 +22,7 @@ import {
putTickerResetApi,
putTickerTelegramApi,
putTickerUsersApi,
putTickerWebsitesApi,
} from './Ticker'
import { User } from './User'

Expand Down Expand Up @@ -347,6 +350,91 @@ describe('putTickerResetApi', () => {
})
})

describe('putTickerWebsitesApi', () => {
beforeEach(() => {
fetchMock.resetMocks()
})

it('should return data on success', async () => {
const data = { status: 'success', data: { ticker: { id: 1 } } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await putTickerWebsitesApi('token', { id: 1 } as Ticker, [{ origin: 'origin' } as TickerWebsite])
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'put',
body: JSON.stringify({ websites: [{ origin: 'origin' }] }),
})
})

it('should throw error on non-200 status', async () => {
const data = { status: 'error', error: { code: 500, message: 'Internal Server Error' } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await putTickerWebsitesApi('token', { id: 1 } as Ticker, [{ origin: 'origin' } as TickerWebsite])
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'put',
body: JSON.stringify({ websites: [{ origin: 'origin' } as TickerWebsite] }),
})
})

it('should throw error on network error', async () => {
fetchMock.mockReject(new Error('Network error'))
const response = await putTickerWebsitesApi('token', { id: 1 } as Ticker, [{ origin: 'origin' } as TickerWebsite])
expect(response).toEqual({ status: 'error', error: { code: 500, message: 'Network error' } })
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'put',
body: JSON.stringify({ websites: [{ origin: 'origin' } as TickerWebsite] }),
})
})
})

describe('deleteTickerWebsitesApi', () => {
beforeEach(() => {
fetchMock.resetMocks()
})

it('should return data on success', async () => {
const data = { status: 'success', data: { ticker: { id: 1 } } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await deleteTickerWebsitesApi('token', { id: 1 } as Ticker)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'delete',
})
})

it('should throw error on non-200 status', async () => {
const data = { status: 'error', error: { code: 500, message: 'Internal Server Error' } }
fetchMock.mockResponseOnce(JSON.stringify(data))
const response = await deleteTickerWebsitesApi('token', { id: 1 } as Ticker)
expect(response).toEqual(data)
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'delete',
})
})

it('should throw error on network error', async () => {
fetchMock.mockReject(new Error('Network error'))
const response = await deleteTickerWebsitesApi('token', { id: 1 } as Ticker)
expect(response).toEqual({ status: 'error', error: { code: 500, message: 'Network error' } })
expect(fetchMock).toHaveBeenCalledTimes(1)
expect(fetchMock).toHaveBeenCalledWith(`${ApiUrl}/admin/tickers/1/websites`, {
headers: apiHeaders('token'),
method: 'delete',
})
})
})

describe('putTickerMastodonApi', () => {
beforeEach(() => {
fetchMock.resetMocks()
Expand Down
24 changes: 22 additions & 2 deletions src/api/Ticker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ export interface TickerFormData {
export interface Ticker {
id: number
createdAt: Date
domain: string
title: string
description: string
active: boolean
information: TickerInformation
websites: Array<TickerWebsite>
mastodon: TickerMastodon
telegram: TickerTelegram
bluesky: TickerBluesky
Expand All @@ -53,6 +53,11 @@ export interface TickerInformation {
bluesky: string
}

export interface TickerWebsite {
id: number
origin: string
}

export interface TickerTelegram {
active: boolean
connected: boolean
Expand Down Expand Up @@ -118,7 +123,7 @@ export interface TickerLocation {

export interface GetTickersQueryParams {
active?: boolean
domain?: string
origin?: string
title?: string
order_by?: string
sort?: SortDirection
Expand Down Expand Up @@ -172,6 +177,21 @@ export async function putTickerResetApi(token: string, ticker: Ticker): Promise<
return apiClient<TickerResponseData>(`${ApiUrl}/admin/tickers/${ticker.id}/reset`, { headers: apiHeaders(token), method: 'put' })
}

export async function putTickerWebsitesApi(token: string, ticker: Ticker, websites: Array<TickerWebsite>): Promise<ApiResponse<TickerResponseData>> {
return apiClient<TickerResponseData>(`${ApiUrl}/admin/tickers/${ticker.id}/websites`, {
headers: apiHeaders(token),
method: 'put',
body: JSON.stringify({ websites: websites }),
})
}

export async function deleteTickerWebsitesApi(token: string, ticker: Ticker): Promise<ApiResponse<TickerResponseData>> {
return apiClient<TickerResponseData>(`${ApiUrl}/admin/tickers/${ticker.id}/websites`, {
headers: apiHeaders(token),
method: 'delete',
})
}

export async function putTickerMastodonApi(token: string, data: TickerMastodonFormData, ticker: Ticker): Promise<ApiResponse<TickerResponseData>> {
return apiClient<TickerResponseData>(`${ApiUrl}/admin/tickers/${ticker.id}/mastodon`, {
headers: apiHeaders(token),
Expand Down
17 changes: 5 additions & 12 deletions src/components/ticker/TickerCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { faCheck, faHeading, faXmark } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Box, Card, CardContent, Typography } from '@mui/material'
import { FC } from 'react'
import { Box, Card, CardContent, Link, Typography } from '@mui/material'
import { Ticker } from '../../api/Ticker'
import NamedListItem from '../common/NamedListItem'
import SocialConnectionChip from './SocialConnectionChip'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheck, faHeading, faLink, faXmark } from '@fortawesome/free-solid-svg-icons'

interface Props {
ticker: Ticker
Expand All @@ -26,16 +26,9 @@ const TickerCard: FC<Props> = ({ ticker }) => {
{ticker.active ? 'Active' : 'Inactive'}
</Typography>
</NamedListItem>
<NamedListItem title="Domain">
<Typography>
<FontAwesomeIcon color="GrayText" icon={faLink} style={{ width: 16, paddingRight: 2 }} />
<Link href={'https://' + ticker.domain} rel="noreferrer" target="_blank">
{ticker.domain}
</Link>
</Typography>
</NamedListItem>
<NamedListItem title="Social Connections">
<NamedListItem title="Integrations">
<Box>
<SocialConnectionChip active={ticker.websites.length > 0} label="Website" />
<SocialConnectionChip active={ticker.mastodon.active} label="Mastodon" />
<SocialConnectionChip active={ticker.telegram.active} label="Telegram" />
<SocialConnectionChip active={ticker.bluesky.active} label="Bluesky" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { Grid } from '@mui/material'
import { FC } from 'react'
import { Ticker } from '../../api/Ticker'
import MastodonCard from './MastodonCard'
import TelegramCard from './TelegramCard'
import BlueskyCard from './BlueskyCard'
import MastodonCard from './MastodonCard'
import SignalGroupCard from './SignalGroupCard'
import TelegramCard from './TelegramCard'
import WebsiteCard from './WebsiteCard'

interface Props {
ticker: Ticker
}

const TickerSocialConnections: FC<Props> = ({ ticker }) => {
const TickerIntegrations: FC<Props> = ({ ticker }) => {
return (
<Grid container spacing={2}>
<Grid item md={6} xs={12}>
<WebsiteCard ticker={ticker} />
</Grid>
<Grid item md={6} xs={12}>
<MastodonCard ticker={ticker} />
</Grid>
Expand All @@ -29,4 +33,4 @@ const TickerSocialConnections: FC<Props> = ({ ticker }) => {
)
}

export default TickerSocialConnections
export default TickerIntegrations
10 changes: 3 additions & 7 deletions src/components/ticker/TickerList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const TickerList: FC<Props> = ({ token }) => {
if (params.order_by) newSearchParams.set('order_by', params.order_by)
if (params.sort) newSearchParams.set('sort', params.sort)
if (params.title) newSearchParams.set('title', params.title)
if (params.domain) newSearchParams.set('domain', params.domain)
if (params.origin) newSearchParams.set('domain', params.origin)
if (params.active !== undefined) newSearchParams.set('active', String(params.active))
setSearchParams(newSearchParams)
}, [debouncedValue, params, setSearchParams])
Expand Down Expand Up @@ -64,7 +64,7 @@ const TickerList: FC<Props> = ({ token }) => {
<TableHead>
<TableRow>
<TableCell colSpan={5} sx={{ p: 1 }}>
<TickerListFilter params={params} onTitleChange={handleFilterChange} onDomainChange={handleFilterChange} onActiveChange={handleActiveChange} />
<TickerListFilter params={params} onTitleChange={handleFilterChange} onOriginChange={handleFilterChange} onActiveChange={handleActiveChange} />
</TableCell>
</TableRow>
<TableRow>
Expand All @@ -83,11 +83,7 @@ const TickerList: FC<Props> = ({ token }) => {
Title
</TableSortLabel>
</TableCell>
<TableCell sortDirection={setDirection('domain')}>
<TableSortLabel active={sortActive('domain')} direction={setDirection('domain')} onClick={() => handleSortChange('domain')}>
Domain
</TableSortLabel>
</TableCell>
<TableCell sortDirection={setDirection('origin')}>Web Origins</TableCell>
<TableCell />
</TableRow>
</TableHead>
Expand Down
44 changes: 22 additions & 22 deletions src/components/ticker/TickerListFilter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,67 @@
import { render, screen } from '@testing-library/react'
import TickerListFilter from './TickerListFilter'
import { GetTickersQueryParams } from '../../api/Ticker'
import userEvent from '@testing-library/user-event'
import { GetTickersQueryParams } from '../../api/Ticker'
import TickerListFilter from './TickerListFilter'

describe('TickerListFilter', async function () {
it('should render', function () {
const onTitleChange = vi.fn()
const onDomainChange = vi.fn()
const onOriginChange = vi.fn()
const onActiveChange = vi.fn()
const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams
const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams

render(<TickerListFilter params={params} onTitleChange={onTitleChange} onDomainChange={onDomainChange} onActiveChange={onActiveChange} />)
render(<TickerListFilter params={params} onTitleChange={onTitleChange} onOriginChange={onOriginChange} onActiveChange={onActiveChange} />)

expect(onTitleChange).not.toHaveBeenCalled()
expect(onDomainChange).not.toHaveBeenCalled()
expect(onOriginChange).not.toHaveBeenCalled()
expect(onActiveChange).not.toHaveBeenCalled()

expect(screen.getByLabelText('Title')).toBeInTheDocument()
expect(screen.getByLabelText('Domain')).toBeInTheDocument()
expect(screen.getByLabelText('Origin')).toBeInTheDocument()
expect(screen.getByText('All')).toBeInTheDocument()
expect(screen.getByText('Active')).toBeInTheDocument()
expect(screen.getByText('Inactive')).toBeInTheDocument()

expect(screen.getByLabelText('Title')).toHaveValue('')
expect(screen.getByLabelText('Domain')).toHaveValue('')
expect(screen.getByLabelText('Origin')).toHaveValue('')
expect(screen.getByText('All')).toHaveClass('Mui-selected')
})

it('should call onTitleChange', async function () {
const onTitleChange = vi.fn()
const onDomainChange = vi.fn()
const onOriginChange = vi.fn()
const onActiveChange = vi.fn()
const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams
const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams

render(<TickerListFilter params={params} onTitleChange={onTitleChange} onDomainChange={onDomainChange} onActiveChange={onActiveChange} />)
render(<TickerListFilter params={params} onTitleChange={onTitleChange} onOriginChange={onOriginChange} onActiveChange={onActiveChange} />)

await userEvent.type(screen.getByLabelText('Title'), 'foo')
expect(onTitleChange).toHaveBeenCalledWith('title', 'f')
expect(onTitleChange).toHaveBeenCalledWith('title', 'o')
expect(onTitleChange).toHaveBeenCalledWith('title', 'o')
})

it('should call onDomainChange', async function () {
it('should call onOriginChange', async function () {
const onTitleChange = vi.fn()
const onDomainChange = vi.fn()
const onOriginChange = vi.fn()
const onActiveChange = vi.fn()
const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams
const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams

render(<TickerListFilter params={params} onTitleChange={onTitleChange} onDomainChange={onDomainChange} onActiveChange={onActiveChange} />)
render(<TickerListFilter params={params} onTitleChange={onTitleChange} onOriginChange={onOriginChange} onActiveChange={onActiveChange} />)

await userEvent.type(screen.getByLabelText('Domain'), 'foo')
expect(onDomainChange).toHaveBeenCalledWith('domain', 'f')
expect(onDomainChange).toHaveBeenCalledWith('domain', 'o')
expect(onDomainChange).toHaveBeenCalledWith('domain', 'o')
await userEvent.type(screen.getByLabelText('Origin'), 'foo')
expect(onOriginChange).toHaveBeenCalledWith('origin', 'f')
expect(onOriginChange).toHaveBeenCalledWith('origin', 'o')
expect(onOriginChange).toHaveBeenCalledWith('origin', 'o')
})

it('should call onActiveChange', async function () {
const onTitleChange = vi.fn()
const onDomainChange = vi.fn()
const onOriginChange = vi.fn()
const onActiveChange = vi.fn()
const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams
const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams

render(<TickerListFilter params={params} onTitleChange={onTitleChange} onDomainChange={onDomainChange} onActiveChange={onActiveChange} />)
render(<TickerListFilter params={params} onTitleChange={onTitleChange} onOriginChange={onOriginChange} onActiveChange={onActiveChange} />)

await userEvent.click(screen.getByText('Active'))
expect(onActiveChange).toHaveBeenCalledWith(expect.anything(), 'true')
Expand Down
6 changes: 3 additions & 3 deletions src/components/ticker/TickerListFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { GetTickersQueryParams } from '../../api/Ticker'
interface Props {
params: GetTickersQueryParams
onTitleChange: (field: string, value: string) => void
onDomainChange: (field: string, value: string) => void
onOriginChange: (field: string, value: string) => void
onActiveChange: (e: React.MouseEvent<HTMLElement, MouseEvent>, value: unknown) => void
}

const TickerListFilter: FC<Props> = ({ params, onTitleChange, onDomainChange, onActiveChange }) => {
const TickerListFilter: FC<Props> = ({ params, onTitleChange, onOriginChange, onActiveChange }) => {
return (
<Stack direction="row" alignItems="center">
<Box sx={{ px: 1 }}>
Expand All @@ -28,7 +28,7 @@ const TickerListFilter: FC<Props> = ({ params, onTitleChange, onDomainChange, on
/>
</Box>
<Box sx={{ px: 1 }}>
<TextField label="Domain" onChange={e => onDomainChange('domain', e.target.value)} placeholder="Filter by domain" size="small" value={params.domain} />
<TextField label="Origin" onChange={e => onOriginChange('origin', e.target.value)} placeholder="Filter by origin" size="small" value={params.origin} />
</Box>
<Box sx={{ px: 1 }}>
<ToggleButtonGroup size="small" value={params.active} exclusive onChange={onActiveChange}>
Expand Down
Loading

0 comments on commit d706ba2

Please sign in to comment.