Skip to content

Commit

Permalink
merge main into branch
Browse files Browse the repository at this point in the history
  • Loading branch information
neilcampbell committed Apr 8, 2024
2 parents f84cec8 + ee32e8b commit 6902393
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 6 deletions.
5 changes: 4 additions & 1 deletion src/App.routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import { evalTemplates } from './routes/templated-route'
import { TransactionPage } from './features/transactions/pages/transaction-page'
import { ExplorePage } from './features/explore/pages/explore-page'
import { GroupPage } from './features/transactions/pages/group-page'
import ErrorBoundary from './features/errors/components/error-boundary'

export const routes = evalTemplates([
{
template: Urls.Index,
element: (
<LayoutPage>
<Outlet />
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
</LayoutPage>
),
children: [
Expand Down
47 changes: 47 additions & 0 deletions src/features/errors/components/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Component, ErrorInfo, PropsWithChildren } from 'react'

type ErrorBoundaryProps = PropsWithChildren

function ErrorBoundary(props: ErrorBoundaryProps) {
return <ErrorBoundaryImpl {...{ ...props }} />
}

type ErrorBoundaryImplProps = ErrorBoundaryProps
type ErrorBoundaryImplState = {
errorType: ErrorType
}

enum ErrorType {
None,
Other,
}

class ErrorBoundaryImpl extends Component<ErrorBoundaryImplProps, ErrorBoundaryImplState> {
constructor(props: ErrorBoundaryImplProps) {
super(props)

this.state = {
errorType: ErrorType.None,
}
}

public static getDerivedStateFromError(_e: unknown): ErrorBoundaryImplState {
return {
errorType: ErrorType.Other,
}
}

public async componentDidCatch(_e: unknown, _errorInfo: ErrorInfo) {}

public componentDidUpdate() {}

public render() {
if (this.state.errorType === ErrorType.None) {
return this.props.children
}

return <p>Error</p>
}
}

export default ErrorBoundary
3 changes: 0 additions & 3 deletions src/features/layout/components/connect-wallet.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { Button } from '@/features/common/components/button'
import { Input } from '@/features/common/components/input'
import { cn } from '@/features/common/utils'

export function ConnectWallet() {
return (
<div className={cn('flex gap-2')}>
{/* TODO: add search icon */}
<Input className={cn('w-96')} />
<Button>Connect wallet</Button>
</div>
)
Expand Down
2 changes: 2 additions & 0 deletions src/features/layout/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { cn } from '@/features/common/utils'
import { ThemeToggle } from '@/features/theme/components/theme-toggle'
import { NetworkSelect } from './network-select'
import { ConnectWallet } from './connect-wallet'
import { Search } from './search'

type Props = {
className?: string
Expand All @@ -10,6 +11,7 @@ type Props = {
export function Header({ className }: Props) {
return (
<div className={cn('bg-card flex h-20 flex-row items-end justify-end gap-8 pb-2 pr-4', className)}>
<Search />
<ConnectWallet />
<NetworkSelect />
<ThemeToggle />
Expand Down
63 changes: 63 additions & 0 deletions src/features/layout/components/search.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Search } from './search'
import { describe, it, expect, vi } from 'vitest'
import { render, waitFor } from '@/tests/testing-library'
import { executeComponentTest } from '@/tests/test-component'
import { useNavigate } from 'react-router-dom'

vi.mock('react-router-dom', () => ({
...vi.importActual('react-router-dom'),
useNavigate: vi.fn(),
}))

describe('search', () => {
it('should render search input and button', () => {
return executeComponentTest(
() => render(<Search />),
async (component) => {
await waitFor(() => {
expect(component.getByPlaceholderText('Search')).not.toBeNull()
expect(component.getByRole('button', { name: 'search' })).not.toBeNull()
})
}
)
})

it('should call navigate when search button is clicked', () => {
const mockNavigate = vi.fn()
vi.mocked(useNavigate).mockReturnValue(mockNavigate)

return executeComponentTest(
() => render(<Search />),
async (component, user) => {
await waitFor(async () => {
const input = component.getByPlaceholderText('Search')
const button = component.getByRole('button', { name: 'search' })

await user.type(input, '123456')
await user.click(button)

expect(mockNavigate).toHaveBeenCalledWith('/explore/transaction/123456')
expect(input).toHaveProperty('value', '')
})
}
)
})

it('should not call navigate when search button is clicked and search query is empty', () => {
const mockNavigate = vi.fn()
vi.mocked(useNavigate).mockReturnValue(mockNavigate)

return executeComponentTest(
() => render(<Search />),
async (component, user) => {
await waitFor(async () => {
const button = component.getByRole('button', { name: 'search' })

await user.click(button)

expect(mockNavigate).not.toHaveBeenCalled()
})
}
)
})
})
39 changes: 39 additions & 0 deletions src/features/layout/components/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Button } from '@/features/common/components/button'
import { Input } from '@/features/common/components/input'
import { cn } from '@/features/common/utils'
import { Urls } from '@/routes/urls'
import { useCallback, useState } from 'react'
import { useNavigate } from 'react-router-dom'

export function Search() {
const [searchQuery, setSearchQuery] = useState<string>('')
const navigate = useNavigate()

const handleInput = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(event.target.value)
}, [])

const doSearch = useCallback(() => {
if (!searchQuery) {
return
}
navigate(Urls.Explore.Transaction.ById.build({ transactionId: searchQuery }))
setSearchQuery('')
}, [navigate, searchQuery])

const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
doSearch()
}
},
[doSearch]
)

return (
<div className={cn('flex gap-2')}>
<Input className={cn('w-96')} placeholder="Search" value={searchQuery} onChange={handleInput} onKeyDown={handleKeyDown} />
<Button onClick={doSearch}>search</Button>
</div>
)
}
5 changes: 5 additions & 0 deletions src/features/theme/constant.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export const themeConstants = {
toggleButtonName: 'Toggle theme',
}

export const transactionPageConstants = {
transactionNotFound: 'Transaction not found',
genericError: 'Error loading transaction',
}
21 changes: 21 additions & 0 deletions src/features/transactions/pages/transaction-page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect, vi } from 'vitest'
import { render } from '@/tests/testing-library'
import { executeComponentTest } from '@/tests/test-component'
import { TransactionPage } from '@/features/transactions/pages/transaction-page'
import { transactionPageConstants } from '@/features/theme/constant'

vi.mock('react-router-dom', () => ({
...vi.importActual('react-router-dom'),
useParams: () => ({ transactionId: 'invalid-id' }),
}))

describe('transaction', () => {
it.skip('should show "Transaction does not exist" for an invalid transaction ID', () => {
return executeComponentTest(
() => render(<TransactionPage />),
async (component) => {
expect(component.getByText(transactionPageConstants.transactionNotFound)).toBeTruthy()
}
)
})
})
10 changes: 8 additions & 2 deletions src/features/transactions/pages/transaction-page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import invariant from 'tiny-invariant'
import { UrlParams } from '../../../routes/urls'
import { useRequiredParam } from '../../common/hooks/use-required-param'
import { Transaction } from '../components/transaction'
import { useLoadableTransaction } from '../data'
import { transactionPageConstants } from '@/features/theme/constant'

export const isValidTransactionId = (transactionId: string) => transactionId.length === 52

export function TransactionPage() {
const { transactionId } = useRequiredParam(UrlParams.TransactionId)
invariant(isValidTransactionId(transactionId), 'transactionId is invalid')
const loadableTransaction = useLoadableTransaction(transactionId)

if (loadableTransaction.state === 'hasData') {
return <Transaction transaction={loadableTransaction.data} />
} else if (loadableTransaction.state === 'loading') {
// TODO: Make this a spinner
return <p>Loading....</p>
}

Expand All @@ -19,8 +25,8 @@ export function TransactionPage() {
'status' in loadableTransaction.error &&
loadableTransaction.error.status === 404
) {
return <p>Error: Transaction not found</p>
return <p>{transactionPageConstants.transactionNotFound}</p>
}

return <p>Error: Transaction failed to load</p>
return <p>{transactionPageConstants.genericError}</p>
}

0 comments on commit 6902393

Please sign in to comment.