diff --git a/src/api/Ticker.test.ts b/src/api/Ticker.test.ts index cc9c3a12..057459ba 100644 --- a/src/api/Ticker.test.ts +++ b/src/api/Ticker.test.ts @@ -5,11 +5,13 @@ import { TickerFormData, TickerMastodonFormData, TickerTelegramFormData, + TickerWebsite, deleteTickerApi, deleteTickerBlueskyApi, deleteTickerMastodonApi, deleteTickerTelegramApi, deleteTickerUserApi, + deleteTickerWebsitesApi, fetchTickerApi, fetchTickerUsersApi, fetchTickersApi, @@ -20,6 +22,7 @@ import { putTickerResetApi, putTickerTelegramApi, putTickerUsersApi, + putTickerWebsitesApi, } from './Ticker' import { User } from './User' @@ -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() diff --git a/src/api/Ticker.ts b/src/api/Ticker.ts index be09a331..85af292d 100644 --- a/src/api/Ticker.ts +++ b/src/api/Ticker.ts @@ -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 mastodon: TickerMastodon telegram: TickerTelegram bluesky: TickerBluesky @@ -53,6 +53,11 @@ export interface TickerInformation { bluesky: string } +export interface TickerWebsite { + id: number + origin: string +} + export interface TickerTelegram { active: boolean connected: boolean @@ -118,7 +123,7 @@ export interface TickerLocation { export interface GetTickersQueryParams { active?: boolean - domain?: string + origin?: string title?: string order_by?: string sort?: SortDirection @@ -172,6 +177,21 @@ export async function putTickerResetApi(token: string, ticker: Ticker): Promise< return apiClient(`${ApiUrl}/admin/tickers/${ticker.id}/reset`, { headers: apiHeaders(token), method: 'put' }) } +export async function putTickerWebsitesApi(token: string, ticker: Ticker, websites: Array): Promise> { + return apiClient(`${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> { + return apiClient(`${ApiUrl}/admin/tickers/${ticker.id}/websites`, { + headers: apiHeaders(token), + method: 'delete', + }) +} + export async function putTickerMastodonApi(token: string, data: TickerMastodonFormData, ticker: Ticker): Promise> { return apiClient(`${ApiUrl}/admin/tickers/${ticker.id}/mastodon`, { headers: apiHeaders(token), diff --git a/src/components/ticker/TickerCard.tsx b/src/components/ticker/TickerCard.tsx index 1d1a92b6..e652f95c 100644 --- a/src/components/ticker/TickerCard.tsx +++ b/src/components/ticker/TickerCard.tsx @@ -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 @@ -26,16 +26,9 @@ const TickerCard: FC = ({ ticker }) => { {ticker.active ? 'Active' : 'Inactive'} - - - - - {ticker.domain} - - - - + + 0} label="Website" /> diff --git a/src/components/ticker/TickerSocialConnections.tsx b/src/components/ticker/TickerIntegrations.tsx similarity index 77% rename from src/components/ticker/TickerSocialConnections.tsx rename to src/components/ticker/TickerIntegrations.tsx index 4e13cf71..3f5190ed 100644 --- a/src/components/ticker/TickerSocialConnections.tsx +++ b/src/components/ticker/TickerIntegrations.tsx @@ -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 = ({ ticker }) => { +const TickerIntegrations: FC = ({ ticker }) => { return ( + + + @@ -29,4 +33,4 @@ const TickerSocialConnections: FC = ({ ticker }) => { ) } -export default TickerSocialConnections +export default TickerIntegrations diff --git a/src/components/ticker/TickerList.tsx b/src/components/ticker/TickerList.tsx index 43e994f1..09497372 100644 --- a/src/components/ticker/TickerList.tsx +++ b/src/components/ticker/TickerList.tsx @@ -21,7 +21,7 @@ const TickerList: FC = ({ 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]) @@ -64,7 +64,7 @@ const TickerList: FC = ({ token }) => { - + @@ -83,11 +83,7 @@ const TickerList: FC = ({ token }) => { Title - - handleSortChange('domain')}> - Domain - - + Web Origins diff --git a/src/components/ticker/TickerListFilter.test.tsx b/src/components/ticker/TickerListFilter.test.tsx index 311a5c8d..f7521bfb 100644 --- a/src/components/ticker/TickerListFilter.test.tsx +++ b/src/components/ticker/TickerListFilter.test.tsx @@ -1,39 +1,39 @@ 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() + render() 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() + render() await userEvent.type(screen.getByLabelText('Title'), 'foo') expect(onTitleChange).toHaveBeenCalledWith('title', 'f') @@ -41,27 +41,27 @@ describe('TickerListFilter', async function () { 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() + render() - 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() + render() await userEvent.click(screen.getByText('Active')) expect(onActiveChange).toHaveBeenCalledWith(expect.anything(), 'true') diff --git a/src/components/ticker/TickerListFilter.tsx b/src/components/ticker/TickerListFilter.tsx index 706cf3bf..6581ef27 100644 --- a/src/components/ticker/TickerListFilter.tsx +++ b/src/components/ticker/TickerListFilter.tsx @@ -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, value: unknown) => void } -const TickerListFilter: FC = ({ params, onTitleChange, onDomainChange, onActiveChange }) => { +const TickerListFilter: FC = ({ params, onTitleChange, onOriginChange, onActiveChange }) => { return ( @@ -28,7 +28,7 @@ const TickerListFilter: FC = ({ params, onTitleChange, onDomainChange, on /> - onDomainChange('domain', e.target.value)} placeholder="Filter by domain" size="small" value={params.domain} /> + onOriginChange('origin', e.target.value)} placeholder="Filter by origin" size="small" value={params.origin} /> diff --git a/src/components/ticker/TickerListItem.tsx b/src/components/ticker/TickerListItem.tsx index 75c4f578..59957a3d 100644 --- a/src/components/ticker/TickerListItem.tsx +++ b/src/components/ticker/TickerListItem.tsx @@ -1,11 +1,11 @@ +import { faCheck, faHandPointer, faPencil, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { MoreVert } from '@mui/icons-material' +import { colors, IconButton, MenuItem, Popover, TableCell, TableRow, Typography } from '@mui/material' import React, { FC, useState } from 'react' import { useNavigate } from 'react-router' import { Ticker } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' -import { colors, IconButton, MenuItem, Popover, TableCell, TableRow, Typography } from '@mui/material' -import { MoreVert } from '@mui/icons-material' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faCheck, faHandPointer, faPencil, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons' import TickerModalDelete from './TickerModalDelete' import TickerModalForm from './TickerModalForm' @@ -32,6 +32,8 @@ const TickerListItem: FC = ({ ticker }: Props) => { navigate(`/ticker/${ticker.id}`) } + const origins = ticker.websites.length > 0 ? ticker.websites.map(website => website.origin).join(', ') : 'No origins' + return ( @@ -41,7 +43,7 @@ const TickerListItem: FC = ({ ticker }: Props) => { {ticker.active ? : } {ticker.title} - {ticker.domain} + {origins} diff --git a/src/components/ticker/TickerListItems.test.tsx b/src/components/ticker/TickerListItems.test.tsx index 2064298b..b933928e 100644 --- a/src/components/ticker/TickerListItems.test.tsx +++ b/src/components/ticker/TickerListItems.test.tsx @@ -46,7 +46,7 @@ describe('TickerListItems', function () { vi.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValue(jwt('admin')) fetchMock.mockResponseOnce(JSON.stringify({ data: { tickers: [] }, status: 'success' })) - const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams + const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams setup(params) expect(screen.getByText('Loading')).toBeInTheDocument() @@ -65,10 +65,10 @@ describe('TickerListItems', function () { { id: 1, createdAt: new Date(), - domain: 'localhost', title: 'title', description: 'description', active: true, + websites: [{ id: 1, origin: 'http://localhost' }], }, ], }, @@ -76,7 +76,7 @@ describe('TickerListItems', function () { }) ) - const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams + const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams setup(params) expect(screen.getByText('Loading')).toBeInTheDocument() @@ -84,7 +84,7 @@ describe('TickerListItems', function () { expect(fetchMock).toHaveBeenCalledTimes(1) expect(await screen.findByText('title')).toBeInTheDocument() - expect(await screen.findByText('localhost')).toBeInTheDocument() + expect(await screen.findByText('http://localhost')).toBeInTheDocument() }) it('should render tickers for user', async function () { @@ -96,7 +96,6 @@ describe('TickerListItems', function () { { id: 1, createdAt: new Date(), - domain: 'localhost', title: 'title', description: 'description', active: true, @@ -107,7 +106,7 @@ describe('TickerListItems', function () { }) ) - const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams + const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams setup(params) expect(screen.getByText('Loading')).toBeInTheDocument() @@ -119,7 +118,7 @@ describe('TickerListItems', function () { vi.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValue(jwt('admin')) fetchMock.mockRejectOnce(new Error('bad url')) - const params = { title: '', domain: '', active: undefined } as GetTickersQueryParams + const params = { title: '', origin: '', active: undefined } as GetTickersQueryParams setup(params) expect(screen.getByText('Loading')).toBeInTheDocument() diff --git a/src/components/ticker/TickerModalForm.tsx b/src/components/ticker/TickerModalForm.tsx index b11cf862..e6a72930 100644 --- a/src/components/ticker/TickerModalForm.tsx +++ b/src/components/ticker/TickerModalForm.tsx @@ -1,12 +1,12 @@ -import { Tab, Tabs } from '@mui/material' -import Tune from '@mui/icons-material/Tune' import Campaign from '@mui/icons-material/Campaign' +import Tune from '@mui/icons-material/Tune' +import { Tab, Tabs } from '@mui/material' import React, { FC, useState } from 'react' import { Ticker } from '../../api/Ticker' import Modal from '../common/Modal' import TabPanel from '../common/TabPanel' -import TickerSocialConnections from './TickerSocialConnections' import TickerForm from './form/TickerForm' +import TickerIntegrations from './TickerIntegrations' interface Props { onClose: () => void @@ -25,14 +25,14 @@ const TickerModalForm: FC = ({ onClose, open, ticker }) => { } iconPosition="start" label="General" /> - } iconPosition="start" label="Social Connections" /> + } iconPosition="start" label="Integrations" /> {ticker ? ( - + ) : null} diff --git a/src/components/ticker/WebsiteCard.tsx b/src/components/ticker/WebsiteCard.tsx new file mode 100644 index 00000000..cf93285d --- /dev/null +++ b/src/components/ticker/WebsiteCard.tsx @@ -0,0 +1,80 @@ +import { faGear, faGlobe, faTrash } from '@fortawesome/free-solid-svg-icons' +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 { deleteTickerWebsitesApi, Ticker } from '../../api/Ticker' +import useAuth from '../../contexts/useAuth' +import WebsiteModalForm from './WebsiteModalForm' + +interface Props { + ticker: Ticker +} + +const WebsiteCard: FC = ({ ticker }) => { + const { token } = useAuth() + const [open, setOpen] = useState(false) + + const queryClient = useQueryClient() + + const websites = ticker.websites + + const handleDelete = useCallback(() => { + deleteTickerWebsitesApi(token, ticker).finally(() => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + }) + }, [token, queryClient, ticker]) + + const links = websites.map(website => ( + + + {website.origin} + + {websites.indexOf(website) < websites.length - 1 ? ', ' : ''} + + )) + + return ( + + + + + Websites + + + + + + + {websites.length > 0 ? ( + + + You have allowed the following websites to access your ticker: {links} + + + ) : ( + + + No website origins allowed. + + + Without configured website origins, the ticker is not reachable from any website. + + + )} + + {websites.length > 0 ? ( + + + + ) : null} + setOpen(false)} open={open} ticker={ticker} /> + + ) +} + +export default WebsiteCard diff --git a/src/components/ticker/WebsiteForm.test.tsx b/src/components/ticker/WebsiteForm.test.tsx new file mode 100644 index 00000000..20c570f5 --- /dev/null +++ b/src/components/ticker/WebsiteForm.test.tsx @@ -0,0 +1,79 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import sign from 'jwt-encode' +import { MemoryRouter } from 'react-router' +import { Ticker, TickerWebsite } from '../../api/Ticker' +import { AuthProvider } from '../../contexts/AuthContext' +import WebsiteForm from './WebsiteForm' + +const token = sign({ id: 1, email: 'user@example.org', roles: ['user'], exp: new Date().getTime() / 1000 + 600 }, 'secret') + +describe('WebsiteForm', () => { + beforeAll(() => { + localStorage.setItem('token', token) + }) + + const ticker = ({ websites }: { websites: Array }) => { + return { + id: 1, + websites: websites, + } as Ticker + } + + const callback = vi.fn() + + beforeEach(() => { + fetchMock.resetMocks() + }) + + function setup(ticker: Ticker) { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + return render( + + + +
+ + +
+
+
+
+ ) + } + + it('should render the component', async () => { + setup(ticker({ websites: [] })) + + expect(screen.getByRole('button', { name: 'Add Origin' })).toBeInTheDocument() + + await userEvent.click(screen.getByRole('button', { name: 'Add Origin' })) + + expect(screen.getByPlaceholderText('https://example.com')).toBeInTheDocument() + + await userEvent.type(screen.getByPlaceholderText('https://example.com'), 'https://example.com') + + fetchMock.mockResponseOnce(JSON.stringify({ status: 'success' })) + + await userEvent.click(screen.getByRole('button', { name: 'Submit' })) + + expect(callback).toHaveBeenCalledTimes(1) + expect(fetchMock).toHaveBeenCalledTimes(1) + expect(fetchMock).toHaveBeenCalledWith('http://localhost:8080/v1/admin/tickers/1/websites', { + body: JSON.stringify({ websites: [{ origin: 'https://example.com' }] }), + headers: { + Accept: 'application/json', + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/json', + }, + method: 'put', + }) + }) +}) diff --git a/src/components/ticker/WebsiteForm.tsx b/src/components/ticker/WebsiteForm.tsx new file mode 100644 index 00000000..cfd167b9 --- /dev/null +++ b/src/components/ticker/WebsiteForm.tsx @@ -0,0 +1,83 @@ +import { Delete } from '@mui/icons-material' +import { Button, FormControl, FormGroup, FormHelperText, Grid, IconButton, InputAdornment, OutlinedInput, Typography } from '@mui/material' +import { useQueryClient } from '@tanstack/react-query' +import { FC } from 'react' +import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form' +import { putTickerWebsitesApi, Ticker, TickerWebsite } from '../../api/Ticker' +import useAuth from '../../contexts/useAuth' + +interface Props { + callback: () => void + ticker: Ticker +} + +interface FormData { + websites: Array +} + +const WebsiteForm: FC = ({ callback, ticker }) => { + const websites = ticker.websites + const { token } = useAuth() + const { + control, + handleSubmit, + register, + formState: { errors }, + } = useForm({ + defaultValues: { websites }, + }) + const { fields, append, remove } = useFieldArray({ + control, + name: 'websites', + rules: { validate: value => value.length > 0 }, + }) + const queryClient = useQueryClient() + + const onSubmit: SubmitHandler = data => { + putTickerWebsitesApi(token, ticker, data.websites).finally(() => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + callback() + }) + } + + return ( +
+ + + You can configure website origins for your ticker. The ticker will only be reachable from the configured websites. + + + + {fields.map((field, index) => ( + + + remove(index)} edge="end" size="small"> + + + + } + /> + {errors.websites?.[index]?.origin?.message} + + ))} + + + + +
+ ) +} + +export default WebsiteForm diff --git a/src/components/ticker/WebsiteModalForm.tsx b/src/components/ticker/WebsiteModalForm.tsx new file mode 100644 index 00000000..85780370 --- /dev/null +++ b/src/components/ticker/WebsiteModalForm.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react' +import { Ticker } from '../../api/Ticker' +import Modal from '../common/Modal' +import WebsiteForm from './WebsiteForm' + +interface Props { + onClose: () => void + open: boolean + ticker: Ticker +} + +const WebsiteModalForm: FC = ({ onClose, open, ticker }) => { + return ( + + + + ) +} + +export default WebsiteModalForm diff --git a/src/components/ticker/form/Description.tsx b/src/components/ticker/form/Description.tsx index 071f06d8..3e45b755 100644 --- a/src/components/ticker/form/Description.tsx +++ b/src/components/ticker/form/Description.tsx @@ -10,7 +10,16 @@ const Description: FC = () => { name="description" control={control} render={({ field, fieldState: { error } }) => ( - + )} /> ) diff --git a/src/components/ticker/form/Domain.tsx b/src/components/ticker/form/Domain.tsx deleted file mode 100644 index 3db98510..00000000 --- a/src/components/ticker/form/Domain.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { TextField } from '@mui/material' -import { FC } from 'react' -import { Controller, useFormContext } from 'react-hook-form' -import useAuth from '../../../contexts/useAuth' - -const Domain: FC = () => { - const { control } = useFormContext() - const { user } = useAuth() - - return ( - ( - - )} - /> - ) -} - -export default Domain diff --git a/src/components/ticker/form/TickerForm.tsx b/src/components/ticker/form/TickerForm.tsx index 029f774d..94cec93b 100644 --- a/src/components/ticker/form/TickerForm.tsx +++ b/src/components/ticker/form/TickerForm.tsx @@ -10,7 +10,6 @@ import Active from './Active' import Author from './Author' import Bluesky from './Bluesky' import Description from './Description' -import Domain from './Domain' import Email from './Email' import Facebook from './Facebook' import Mastodon from './Mastodon' @@ -29,7 +28,6 @@ const TickerForm: FC = ({ callback, id, ticker }) => { const form = useForm({ defaultValues: { title: ticker?.title, - domain: ticker?.domain, active: ticker?.active, description: ticker?.description, information: { @@ -96,19 +94,14 @@ const TickerForm: FC = ({ callback, id, ticker }) => {
- - - - </FormGroup> - </Grid> - <Grid item sm={6} xs={12}> + <Grid item xs={12}> <FormGroup> - <Domain /> + <Active defaultChecked={ticker?.active} /> </FormGroup> </Grid> <Grid item xs={12}> <FormGroup> - <Active defaultChecked={ticker?.active} /> + <Title /> </FormGroup> </Grid> <Grid item xs={12}> diff --git a/src/views/HomeView.tsx b/src/views/HomeView.tsx index 6b060e78..e67919e2 100644 --- a/src/views/HomeView.tsx +++ b/src/views/HomeView.tsx @@ -16,7 +16,7 @@ const HomeView: FC = () => { order_by: params.get('order_by') ?? 'id', sort: params.get('sort') === 'asc' ? 'asc' : 'desc', title: params.get('title') ?? undefined, - domain: params.get('domain') ?? undefined, + origin: params.get('origin') ?? undefined, active: params.get('active') === 'true' ? true : params.get('active') === 'false' ? false : undefined, }, })