-
Notifications
You must be signed in to change notification settings - Fork 134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add view components #1483
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
64ebe5e
feat: add view components
alessey 78ca481
fix: update string to reactNode
alessey 4610ff4
fix: pr comments
alessey 63520de
fix: lint/tests
alessey 8533dec
fix: test
alessey dc41c27
fix: style updates
alessey 90081bf
fix: add usecallback
alessey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export const baseSvg = ( | ||
<svg | ||
data-testid="ock-baseSvg" | ||
role="img" | ||
aria-label="base" | ||
width="100%" | ||
height="100%" | ||
viewBox="0 0 146 146" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<circle cx="73" cy="73" r="73" fill="#0052FF" /> | ||
<path | ||
d="M73.323 123.729C101.617 123.729 124.553 100.832 124.553 72.5875C124.553 44.343 101.617 21.4463 73.323 21.4463C46.4795 21.4463 24.4581 42.0558 22.271 68.2887H89.9859V76.8864H22.271C24.4581 103.119 46.4795 123.729 73.323 123.729Z" | ||
fill="white" | ||
/> | ||
</svg> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { fill } from '../../styles/theme'; | ||
|
||
export const defaultNFTSvg = ( | ||
<svg | ||
data-testid="ock-defaultNFTSvg" | ||
role="img" | ||
aria-label="loading" | ||
width="100%" | ||
height="100%" | ||
viewBox="0 0 527.008 525" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<rect | ||
fill="#F3F4F6" | ||
width="100%" | ||
height="100%" | ||
className={fill.alternate} | ||
/> | ||
<path | ||
d="M232.062 258.667C232.062 268.125 236.209 276.614 242.783 282.417H284.675C291.249 276.614 295.396 268.125 295.396 258.667C295.396 241.178 281.218 227 263.729 227C246.24 227 232.062 241.178 232.062 258.667ZM265.697 253.74L276.646 257.792L265.697 261.843L261.646 272.792L257.594 261.843L246.646 257.792L257.594 253.74L261.646 242.792L265.697 253.74ZM274.146 237.792L276.172 243.266L281.646 245.292L276.172 247.317L274.146 252.792L272.12 247.317L266.646 245.292L272.12 243.266L274.146 237.792Z" | ||
fill="#6B7280" | ||
className={fill.defaultReverse} | ||
/> | ||
<path | ||
d="M287.479 288.25H240.813L234.979 297H293.312L287.479 288.25Z" | ||
fill="#6B7280" | ||
className={fill.defaultReverse} | ||
/> | ||
</svg> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import '@testing-library/jest-dom'; | ||
import { fireEvent, render } from '@testing-library/react'; | ||
import { | ||
type Mock, | ||
afterEach, | ||
beforeEach, | ||
describe, | ||
expect, | ||
it, | ||
vi, | ||
} from 'vitest'; | ||
import { useNFTContext } from '../NFTProvider'; | ||
import { NFTAudio } from './NFTAudio'; | ||
|
||
const mockAnimationUrl = 'https://example.com/audio.mp3'; | ||
|
||
vi.mock('../NFTProvider', () => ({ | ||
useNFTContext: vi.fn(), | ||
})); | ||
|
||
describe('NFTAudio', () => { | ||
beforeEach(() => { | ||
(useNFTContext as Mock).mockReturnValue({ | ||
animationUrl: mockAnimationUrl, | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
vi.clearAllMocks(); | ||
}); | ||
|
||
it('should render', () => { | ||
const { getByTestId } = render(<NFTAudio />); | ||
expect(getByTestId('ockNFTAudio')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should call onLoading when audio starts loading', () => { | ||
const onLoading = vi.fn(); | ||
const { getByTestId } = render(<NFTAudio onLoading={onLoading} />); | ||
fireEvent.loadStart(getByTestId('ockNFTAudio')); | ||
expect(onLoading).toHaveBeenCalledWith(mockAnimationUrl); | ||
}); | ||
|
||
it('should call onLoaded when audio is loaded', () => { | ||
const onLoaded = vi.fn(); | ||
const { getByTestId } = render(<NFTAudio onLoaded={onLoaded} />); | ||
fireEvent.loadedData(getByTestId('ockNFTAudio')); | ||
expect(onLoaded).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should handle error when audio fails to load', () => { | ||
const onError = vi.fn(); | ||
|
||
const { getByTestId } = render(<NFTAudio onError={onError} />); | ||
|
||
fireEvent.error(getByTestId('ockNFTAudio')); | ||
|
||
expect(onError).toHaveBeenCalledWith({ | ||
error: 'error', | ||
code: 'NmNAc01', | ||
message: 'Error loading audio', | ||
}); | ||
}); | ||
|
||
it('should handle string error when audio fails to load', () => { | ||
const onError = vi.fn(); | ||
|
||
const { getByTestId } = render(<NFTAudio onError={onError} />); | ||
|
||
getByTestId('ockNFTAudio').onerror?.('string error'); | ||
|
||
expect(onError).toHaveBeenCalledWith({ | ||
error: 'string error', | ||
code: 'NmNAc01', | ||
message: 'Error loading audio', | ||
}); | ||
}); | ||
|
||
it('should reset state when audio ends', () => { | ||
const { getByTestId } = render(<NFTAudio />); | ||
const audio = getByTestId('ockNFTAudio') as HTMLAudioElement; | ||
|
||
fireEvent.ended(audio); | ||
expect(audio.paused).toBe(true); | ||
}); | ||
|
||
it('should play when button is clicked', () => { | ||
vi.spyOn(HTMLMediaElement.prototype, 'play').mockImplementation(() => | ||
Promise.resolve(), | ||
); | ||
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {}); | ||
|
||
const { getByRole, getByTestId } = render(<NFTAudio />); | ||
const button = getByRole('button'); | ||
const audio = getByTestId('ockNFTAudio') as HTMLAudioElement; | ||
|
||
// play | ||
fireEvent.click(button); | ||
expect(audio.play).toHaveBeenCalled(); | ||
|
||
// pause | ||
fireEvent.click(button); | ||
expect(audio.pause).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should not render if animationUrl is not provided', () => { | ||
(useNFTContext as Mock).mockReturnValue({ | ||
animationUrl: null, | ||
}); | ||
const { container } = render(<NFTAudio />); | ||
expect(container).toBeEmptyDOMElement(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { useCallback, useEffect, useRef, useState } from 'react'; | ||
import { background, cn } from '../../../styles/theme'; | ||
import type { NFTError } from '../../types'; | ||
import { useNFTContext } from '../NFTProvider'; | ||
|
||
type NFTAudioReact = { | ||
className?: string; | ||
onLoading?: (mediaUrl: string) => void; | ||
onLoaded?: () => void; | ||
onError?: (error: NFTError) => void; | ||
}; | ||
|
||
export function NFTAudio({ | ||
className, | ||
onLoading, | ||
onLoaded, | ||
onError, | ||
}: NFTAudioReact) { | ||
const { animationUrl } = useNFTContext(); | ||
const audioRef = useRef<HTMLAudioElement>(null); | ||
const [isPlaying, setIsPlaying] = useState(false); | ||
|
||
useEffect(() => { | ||
function onEnded() { | ||
setIsPlaying(false); | ||
} | ||
|
||
if (animationUrl && audioRef?.current) { | ||
audioRef.current.onloadstart = () => { | ||
onLoading?.(animationUrl); | ||
}; | ||
|
||
audioRef.current.onloadeddata = () => { | ||
onLoaded?.(); | ||
}; | ||
|
||
audioRef.current.addEventListener('ended', onEnded); | ||
|
||
audioRef.current.onerror = (error: string | Event) => { | ||
onError?.({ | ||
error: typeof error === 'string' ? error : error.type, | ||
code: 'NmNAc01', // NFT module NFTAudio component 01 error | ||
message: 'Error loading audio', | ||
}); | ||
}; | ||
} | ||
}, [animationUrl, onLoading, onLoaded, onError]); | ||
|
||
const handlePlayPause = useCallback(() => { | ||
if (isPlaying) { | ||
audioRef.current?.pause(); | ||
setIsPlaying(false); | ||
} else { | ||
audioRef.current?.play(); | ||
setIsPlaying(true); | ||
} | ||
}, [isPlaying]); | ||
|
||
if (!animationUrl) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<div className={cn('max-h-350 w-350 max-w-350', className)}> | ||
<button | ||
type="button" | ||
className={cn( | ||
background.reverse, | ||
'ml-6 inline-flex h-[42px] w-[42px] cursor-pointer items-center justify-center rounded-full', | ||
)} | ||
onClick={handlePlayPause} | ||
> | ||
<div | ||
className={cn( | ||
'ml-px box-border h-[18px] transition-all ease-[100ms] will-change-[border-width]', | ||
'border-transparent border-l-[var(--ock-bg-default)] hover:border-l-[var(--ock-bg-default-hover)]', | ||
{ | ||
'border-[length:0_0_0_16px] border-double': isPlaying, | ||
'border-[length:9px_0_9px_16px] border-solid': !isPlaying, | ||
}, | ||
)} | ||
/> | ||
</button> | ||
<audio | ||
ref={audioRef} | ||
data-testid="ockNFTAudio" | ||
autoPlay={false} | ||
controls={false} | ||
src={animationUrl} | ||
> | ||
<track kind="captions" /> | ||
</audio> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import '@testing-library/jest-dom'; | ||
import { fireEvent, render, waitFor } from '@testing-library/react'; | ||
import { act } from 'react'; | ||
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
import { useNFTContext } from '../NFTProvider'; | ||
import { NFTImage } from './NFTImage'; | ||
|
||
const mockContext = { | ||
imageUrl: 'https://example.com/nft-image.png', | ||
description: 'Test NFT Image', | ||
}; | ||
|
||
vi.mock('../NFTProvider', () => ({ | ||
useNFTContext: vi.fn(), | ||
})); | ||
|
||
describe('NFTImage', () => { | ||
beforeEach(() => { | ||
(useNFTContext as Mock).mockReturnValue(mockContext); | ||
|
||
// mock Image constructor to call load/error events based on src | ||
Object.defineProperty(global.Image.prototype, 'src', { | ||
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: test | ||
set(src) { | ||
if (src === 'error' && this.onerror) { | ||
this.onerror('mocked error'); | ||
} | ||
if (src === 'uiEventError' && this.onerror) { | ||
this.onerror({ type: 'mocked error' }); | ||
} | ||
if (src === 'loaded' && this.onload) { | ||
this.onload(); | ||
} | ||
}, | ||
}); | ||
}); | ||
|
||
it('should render default SVG while loading', () => { | ||
const { getByTestId } = render(<NFTImage />); | ||
expect(getByTestId('ock-defaultNFTSvg')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should call onLoading with the image URL', () => { | ||
const onLoading = vi.fn(); | ||
render(<NFTImage onLoading={onLoading} />); | ||
expect(onLoading).toHaveBeenCalledWith(mockContext.imageUrl); | ||
}); | ||
|
||
it('should call onLoaded when the image loads successfully', async () => { | ||
(useNFTContext as Mock).mockReturnValue({ | ||
imageUrl: 'loaded', | ||
description: 'Test NFT Image', | ||
}); | ||
const onLoaded = vi.fn(); | ||
render(<NFTImage onLoaded={onLoaded} />); | ||
await waitFor(() => expect(onLoaded).toHaveBeenCalled()); | ||
}); | ||
|
||
it('should call onError when the image fails to load', async () => { | ||
(useNFTContext as Mock).mockReturnValue({ | ||
imageUrl: 'error', | ||
description: 'Test NFT Image', | ||
}); | ||
const onError = vi.fn(); | ||
render(<NFTImage onError={onError} />); | ||
await waitFor(() => | ||
expect(onError).toHaveBeenCalledWith({ | ||
error: 'mocked error', | ||
code: 'NmNIc01', | ||
message: 'Error loading image', | ||
}), | ||
); | ||
}); | ||
|
||
it('should call onError when there is a uiEvent error', async () => { | ||
(useNFTContext as Mock).mockReturnValue({ | ||
imageUrl: 'uiEventError', | ||
description: 'Test NFT Image', | ||
}); | ||
const onError = vi.fn(); | ||
render(<NFTImage onError={onError} />); | ||
await waitFor(() => | ||
expect(onError).toHaveBeenCalledWith({ | ||
error: 'mocked error', | ||
code: 'NmNIc01', | ||
message: 'Error loading image', | ||
}), | ||
); | ||
}); | ||
|
||
it('should hide the svg on transition end', async () => { | ||
(useNFTContext as Mock).mockReturnValue({ | ||
imageUrl: 'transitionend', | ||
description: 'Test NFT Image', | ||
}); | ||
const { getByTestId, queryByTestId } = render(<NFTImage />); | ||
await act(async () => { | ||
fireEvent.transitionEnd(getByTestId('ockNFTImage')); | ||
}); | ||
expect(queryByTestId('ock-defaultNFTSvg')).toBeNull(); | ||
}); | ||
|
||
it('should retry loading the image when retry button is clicked', async () => { | ||
(useNFTContext as Mock).mockReturnValue({ | ||
imageUrl: 'error', | ||
description: 'Test NFT Image', | ||
}); | ||
const onError = vi.fn(); | ||
const { getByText } = render(<NFTImage onError={onError} />); | ||
await waitFor(() => expect(onError).toHaveBeenCalled()); | ||
|
||
fireEvent.click(getByText('retry')); | ||
await waitFor(() => expect(onError).toHaveBeenCalledTimes(2)); | ||
}); | ||
|
||
it('should apply the provided className', () => { | ||
const { container } = render(<NFTImage className="custom-class" />); | ||
expect(container.firstChild).toHaveClass('custom-class'); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the
fill
always meant to be white here? Is the intention for it to NOT update based off of the current theme?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, I didn't think the base logo should change