Overview
The AgrospAI Data Space Portal provides a comprehensive library of accessible, tested React components.
Atoms
Basic building block components.
Primary interaction component with multiple styles and link support.
style
'primary' | 'ghost' | 'text'
default:"primary"
Visual style variant
External link URL (opens in new tab)
Internal Next.js link path
type
'submit' | 'button'
default:"button"
Button type for forms
Show arrow indicator for internal links
import Button from '@shared/atoms/Button'
// Primary button
<Button onClick={handleClick}>Click Me</Button>
// Ghost style
<Button style="ghost" size="small">Cancel</Button>
// Internal link
<Button to="/publish" arrow>Publish Asset</Button>
// External link
<Button href="https://docs.oceanprotocol.com">Documentation</Button>
// Submit button
<form onSubmit={handleSubmit}>
<Button type="submit" style="primary">Submit</Button>
</form>
Alert
Display important messages with optional actions.
Alert message (supports Markdown)
state
'error' | 'warning' | 'info' | 'success'
required
Alert severity level
Badge text to show next to title
Action button configuration
Dismissal handler (shows close button)
import Alert from '@shared/atoms/Alert'
import { useState } from 'react'
// Simple error alert
<Alert state="error" text="Transaction failed. Please try again." />
// Warning with title
<Alert
state="warning"
title="Network Sync"
text="Subgraph is syncing. Data may be outdated."
/>
// Info with badge
<Alert
state="info"
title="Beta Feature"
badge="NEW"
text="This feature is currently in beta testing."
/>
// With action button
<Alert
state="success"
text="Asset published successfully!"
action={{
name: 'View Asset',
style: 'primary',
handleAction: () => router.push(`/asset/${did}`)
}}
/>
// Dismissible
function DismissibleAlert() {
const [show, setShow] = useState(true)
if (!show) return null
return (
<Alert
state="info"
text="Welcome to the portal!"
onDismiss={() => setShow(false)}
/>
)
}
Loader
Loading spinner with optional message.
Loading message to display
Use white color (for dark backgrounds)
import Loader from '@shared/atoms/Loader'
// Simple loader
<Loader />
// With message
<Loader message="Loading assets..." />
// White variant
<div style={{ background: 'black', padding: '2rem' }}>
<Loader white message="Processing..." />
</div>
// In Suspense boundary
import { Suspense } from 'react'
<Suspense fallback={<Loader message="Loading data..." />}>
<AssetList />
</Suspense>
Modal
Accessible modal dialog component.
Function to toggle modal open/closed
import Modal from '@shared/atoms/Modal'
import Button from '@shared/atoms/Button'
import { useState } from 'react'
function ModalExample() {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
<Modal
title="Confirm Action"
isOpen={isOpen}
onToggleModal={() => setIsOpen(false)}
>
<div>
<p>Are you sure you want to proceed?</p>
<div style={{ display: 'flex', gap: '1rem' }}>
<Button onClick={() => setIsOpen(false)} style="ghost">
Cancel
</Button>
<Button onClick={handleConfirm}>
Confirm
</Button>
</div>
</div>
</Modal>
</>
)
}
Badge
Small label for status or categorization.
import Badge from '@shared/atoms/Badge'
<Badge label="Beta" />
<Badge label="New" className={styles.newBadge} />
// With other components
<h2>
Advanced Features <Badge label="Pro" />
</h2>
Container
Layout container with max-width constraint.
import Container from '@shared/atoms/Container'
<Container>
<h1>Page Title</h1>
<p>Content goes here...</p>
</Container>
Accessible tooltip component.
import Tooltip from '@shared/atoms/Tooltip'
<Tooltip content="Additional information">
<span>Hover me</span>
</Tooltip>
Table
Data table with sorting and pagination support.
import Table from '@shared/atoms/Table'
const columns = [
{ name: 'Name', selector: 'name' },
{ name: 'Price', selector: 'price' },
{ name: 'Owner', selector: 'owner' }
]
const data = [
{ name: 'Dataset 1', price: '10 OCEAN', owner: '0x123...' },
{ name: 'Dataset 2', price: '5 OCEAN', owner: '0x456...' }
]
<Table columns={columns} data={data} />
Molecules
Composite components combining atoms.
Price
Display asset price with token symbol.
Price object from Ocean Protocol
size
'small' | 'mini' | 'large'
Display size
import Price from '@shared/Price'
// Simple price display
<Price price={asset.price} />
// With fees breakdown
<Price
price={asset.price}
orderPriceAndFees={orderDetails}
size="large"
/>
// In asset card
function AssetCard({ asset }) {
return (
<div>
<h3>{asset.metadata.name}</h3>
<Price price={asset.price} size="small" />
</div>
)
}
Publisher
Display publisher information with avatar and address.
import Publisher from '@shared/Publisher'
<Publisher account={asset.nft.owner} />
NetworkName
Display network name with icon.
import NetworkName from '@shared/NetworkName'
<NetworkName networkId={137} />
// Shows "Polygon" with Polygon icon
Pagination controls for lists.
import Pagination from '@shared/Pagination'
import { useState } from 'react'
function AssetList() {
const [page, setPage] = useState(1)
const { data } = useAssets(address, 'dataset', chainId, page)
return (
<>
{data.results.map(asset => (
<AssetCard key={asset.id} asset={asset} />
))}
<Pagination
currentPage={page}
totalPages={data.totalPages}
onPageChange={setPage}
/>
</>
)
}
Time
Format and display timestamps.
import Time from '@shared/atoms/Time'
// Relative time ("2 hours ago")
<Time date={asset.metadata.created} relative />
// Absolute time
<Time date={asset.metadata.created} />
Display asset tags.
import Tags from '@shared/atoms/Tags'
<Tags items={asset.metadata.tags} />
Complex Components
Web3Feedback
Display transaction status and feedback.
import Web3Feedback from '@shared/Web3Feedback'
function TransactionButton() {
const [txHash, setTxHash] = useState()
return (
<>
<Button onClick={handleTransaction}>Execute</Button>
{txHash && <Web3Feedback txHash={txHash} />}
</>
)
}
WalletNetworkSwitcher
Network switching component for multi-chain support.
import WalletNetworkSwitcher from '@shared/WalletNetworkSwitcher'
<WalletNetworkSwitcher />
Page
Page layout wrapper with SEO support.
import Page from '@shared/Page'
function AssetPage({ asset }) {
return (
<Page
title={asset.metadata.name}
description={asset.metadata.description}
uri={`/asset/${asset.id}`}
>
{/* Page content */}
</Page>
)
}
Component Composition
Components are designed to work together:
import Container from '@shared/atoms/Container'
import Alert from '@shared/atoms/Alert'
import Button from '@shared/atoms/Button'
import Loader from '@shared/atoms/Loader'
import Modal from '@shared/atoms/Modal'
import { useState, Suspense } from 'react'
function AssetPublisher() {
const [showModal, setShowModal] = useState(false)
const [error, setError] = useState(null)
return (
<Container>
{error && (
<Alert
state="error"
text={error}
onDismiss={() => setError(null)}
/>
)}
<Button onClick={() => setShowModal(true)}>
Publish New Asset
</Button>
<Modal
title="Publish Asset"
isOpen={showModal}
onToggleModal={() => setShowModal(false)}
>
<Suspense fallback={<Loader message="Loading form..." />}>
<PublishForm onError={setError} />
</Suspense>
</Modal>
</Container>
)
}
Accessibility
All components follow accessibility best practices:
- Semantic HTML elements
- ARIA labels and roles
- Keyboard navigation support
- Focus management
- Screen reader compatibility
// Buttons announce their purpose
<Button aria-label="Download dataset">Download</Button>
// Modals trap focus and support Escape key
<Modal title="Settings" isOpen={open} onToggleModal={close}>
{/* Focus is trapped within modal */}
</Modal>
// Alerts use appropriate ARIA roles
<Alert state="error" text="Error message" />
// Rendered with role="alert" for screen readers
Testing Components
All components include comprehensive test suites:
import { render, screen } from '@testing-library/react'
import Button from '@shared/atoms/Button'
test('Button renders with children', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
test('Button calls onClick handler', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
screen.getByText('Click me').click()
expect(handleClick).toHaveBeenCalledTimes(1)
})
Styling
Components use CSS Modules for scoped styling:
import styles from './MyComponent.module.css'
import Button from '@shared/atoms/Button'
function MyComponent() {
return (
<div className={styles.wrapper}>
<Button className={styles.customButton}>Custom Styled</Button>
</div>
)
}