Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions components/analytics/charts.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { Meta, StoryObj } from '@storybook/react'
import { AnalyticsCharts, ChartSkeleton } from './charts'

type AnalyticsChartsProps = React.ComponentProps<typeof AnalyticsCharts>

const mockSeriesData: AnalyticsChartsProps['series'] = [
{ label: 'Jan 1', count: 5, volume: 1500 },
{ label: 'Jan 8', count: 8, volume: 2400 },
{ label: 'Jan 15', count: 12, volume: 3200 },
{ label: 'Jan 22', count: 10, volume: 2800 },
{ label: 'Jan 29', count: 15, volume: 3800 },
]

const mockTokenShares: AnalyticsChartsProps['tokenShares'] = [
{ symbol: 'USDC', amount: 50000000000n, count: 12, decimals: 6 },
{ symbol: 'USDT', amount: 30000000000n, count: 8, decimals: 6 },
{ symbol: 'EUR', amount: 20000000000n, count: 5, decimals: 6 },
]

const mockTopTokens: AnalyticsChartsProps['topTokens'] = [
{ symbol: 'USDC', amount: 50000000000n, count: 12, decimals: 6 },
{ symbol: 'USDT', amount: 30000000000n, count: 8, decimals: 6 },
{ symbol: 'EUR', amount: 20000000000n, count: 5, decimals: 6 },
]

const mockStatusBreakdown: AnalyticsChartsProps['statusBreakdown'] = [
{ status: 'streaming', count: 15 },
{ status: 'completed', count: 8 },
{ status: 'scheduled', count: 3 },
{ status: 'cancelled', count: 2 },
]

const mockTopRecipients: AnalyticsChartsProps['topRecipients'] = [
{
address: 'GCZJVEY2HJVCWZQW2IGUBNVQ4FXQMYB7VVKQEMKJ3BMPZX5ZMMK5GO7C',
federationName: 'alice.stellar.org',
count: 5,
totals: [
{ symbol: 'USDC', amount: 25000000000n, decimals: 6 },
{ symbol: 'USDT', amount: 15000000000n, decimals: 6 },
],
},
{
address: 'GBXUQR5DNRVAQB46CJMNF4XKLJZL5AOPNOPK3RLVQXPKWCZWUZNAGM3',
federationName: null,
count: 3,
totals: [
{ symbol: 'EUR', amount: 20000000000n, decimals: 6 },
],
},
]

const meta = {
title: 'Analytics/Charts',
component: AnalyticsCharts,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
} satisfies Meta<typeof AnalyticsCharts>

export default meta
type Story = StoryObj<typeof meta>

/** Full analytics dashboard with populated data. */
export const PopulatedData: Story = {
args: {
series: mockSeriesData,
topTokens: mockTopTokens,
tokenShares: mockTokenShares,
totalVolume: 100000000000n,
statusBreakdown: mockStatusBreakdown,
topRecipients: mockTopRecipients,
unlockProgress: { unlocked: 45000000000n, deposited: 100000000000n },
},
}

/** Analytics dashboard with only a single token (USDC). */
export const SingleToken: Story = {
args: {
series: [
{ label: 'Jan 1', count: 5, volume: 1500 },
{ label: 'Jan 8', count: 8, volume: 2400 },
],
topTokens: [
{ symbol: 'USDC', amount: 50000000000n, count: 20, decimals: 6 },
],
tokenShares: [
{ symbol: 'USDC', amount: 50000000000n, count: 20, decimals: 6 },
],
totalVolume: 50000000000n,
statusBreakdown: [
{ status: 'streaming', count: 15 },
{ status: 'completed', count: 5 },
],
topRecipients: [
{
address: 'GCZJVEY2HJVCWZQW2IGUBNVQ4FXQMYB7VVKQEMKJ3BMPZX5ZMMK5GO7C',
federationName: 'alice.stellar.org',
count: 20,
totals: [
{ symbol: 'USDC', amount: 50000000000n, decimals: 6 },
],
},
],
unlockProgress: { unlocked: 25000000000n, deposited: 50000000000n },
},
}

/** Empty analytics state with no data. */
export const EmptyData: Story = {
args: {
series: [],
topTokens: [],
tokenShares: [],
totalVolume: 0n,
statusBreakdown: [],
topRecipients: [],
unlockProgress: { unlocked: 0n, deposited: 0n },
},
}

/** Loading skeleton for analytics charts. */
export const Loading: Story = {
render: () => <ChartSkeleton />,
}
70 changes: 70 additions & 0 deletions components/error-boundary/section-error-boundary.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import { fn, mocked } from 'storybook/test'
import * as sentryModule from '@/lib/sentry'
import { SectionErrorBoundary } from './section-error-boundary'

const meta = {
title: 'Error/SectionErrorBoundary',
component: SectionErrorBoundary,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
beforeEach: () => {
mocked(sentryModule).captureError = fn()
},
} satisfies Meta<typeof SectionErrorBoundary>

export default meta
type Story = StoryObj<typeof meta>

/** Normal child renders without error. */
export const Default: Story = {
args: {
children: (
<div className="p-4 rounded-lg border border-border">
<p>This section rendered successfully.</p>
</div>
),
sectionName: 'Stream list',
},
}

/** Child component that throws an error. */
export const WithError: Story = {
args: {
children: (
<ErrorThrowingChild />
),
sectionName: 'Analytics panel',
onReloadData: fn(),
},
}

/** Multiple sections, one with an error and one working. */
export const MultipleWithErrorBoundary: Story = {
render: () => (
<div className="space-y-4">
<SectionErrorBoundary sectionName="Working section">
<div className="p-4 rounded-lg border border-border">
<p className="text-sm">This section is working normally.</p>
</div>
</SectionErrorBoundary>

<SectionErrorBoundary sectionName="Failed section" onReloadData={fn()}>
<ErrorThrowingChild />
</SectionErrorBoundary>

<SectionErrorBoundary sectionName="Another working section">
<div className="p-4 rounded-lg border border-border">
<p className="text-sm">This section is also working fine.</p>
</div>
</SectionErrorBoundary>
</div>
),
}

function ErrorThrowingChild() {
throw new Error('Something went wrong in this section')
}
61 changes: 61 additions & 0 deletions components/notifications/notification-preferences.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Meta, StoryObj } from '@storybook/react'
import { fn, mocked } from 'storybook/test'
import * as notificationPreferencesModule from '@/hooks/use-notification-preferences'
import { NotificationPreferencesSettings } from './notification-preferences'

const mockUseNotificationPreferences = (onError?: () => void) => ({
preferences: {
stream_created: true,
stream_cancelled: true,
withdrawal: true,
},
setPreference: fn(),
})

const mockUseNotificationPreferencesAllOff = (onError?: () => void) => ({
preferences: {
stream_created: false,
stream_cancelled: false,
withdrawal: false,
},
setPreference: fn(),
})

const meta = {
title: 'Notifications/NotificationPreferencesSettings',
component: NotificationPreferencesSettings,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
beforeEach: () => {
mocked(notificationPreferencesModule).useNotificationPreferences = fn(mockUseNotificationPreferences)
},
} satisfies Meta<typeof NotificationPreferencesSettings>

export default meta
type Story = StoryObj<typeof meta>

/** All notification types enabled (default state). */
export const AllEnabled: Story = {}

/** All notification toggles turned off. */
export const AllDisabled: Story = {
beforeEach: () => {
mocked(notificationPreferencesModule).useNotificationPreferences = fn(mockUseNotificationPreferencesAllOff)
},
}

/** Mixed state with some notifications enabled and others disabled. */
export const PartiallyEnabled: Story = {
beforeEach: () => {
mocked(notificationPreferencesModule).useNotificationPreferences = fn((onError?: () => void) => ({
preferences: {
stream_created: true,
stream_cancelled: false,
withdrawal: true,
},
setPreference: fn(),
}))
},
}
78 changes: 78 additions & 0 deletions components/settings/clear-local-data.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import { mocked } from 'storybook/test'
import * as settingsCopyModule from '@/lib/copy/settings'
import { ClearLocalData } from './clear-local-data'

const mockSettingsCopy = {
clearLocalData: {
title: 'Clear local data',
description: 'Delete all stored preferences, form drafts, and configuration.',
button: 'Clear local data',
confirmPrompt: 'Are you sure? This action cannot be undone.',
confirm: 'Clear everything',
cancel: 'Cancel',
toast: {
cleared: (count: number) => `Cleared ${count} items`,
reloading: 'Page reloading...',
},
},
}

const meta = {
title: 'Settings/ClearLocalData',
component: ClearLocalData,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
beforeEach: () => {
mocked(settingsCopyModule).settingsCopy = mockSettingsCopy as any
},
} satisfies Meta<typeof ClearLocalData>

export default meta
type Story = StoryObj<typeof meta>

/** Default state showing the clear data button. */
export const Default: Story = {}

/** Confirmation step with destructive action. */
export const Confirming: Story = {
render: (args) => {
const Component = (props: React.ComponentProps<typeof ClearLocalData>) => {
const [confirming, setConfirming] = React.useState(true)

// Render by intercepting the component and forcing the confirming state
return (
<div className="max-w-md">
<div className="rounded-lg border border-destructive/40 p-4 space-y-3">
<div className="flex items-start gap-2">
<svg className="size-4 text-destructive shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4v2m0 0v2M7 9h10" />
</svg>
<div>
<p className="text-sm font-medium">{mockSettingsCopy.clearLocalData.title}</p>
<p className="text-xs text-muted-foreground mt-0.5">
{mockSettingsCopy.clearLocalData.description}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{mockSettingsCopy.clearLocalData.confirmPrompt}
</span>
<button className="rounded px-3 py-1.5 text-xs font-medium bg-destructive text-destructive-foreground hover:bg-destructive/90">
{mockSettingsCopy.clearLocalData.confirm}
</button>
<button className="rounded px-3 py-1.5 text-xs font-medium border border-border hover:bg-muted">
{mockSettingsCopy.clearLocalData.cancel}
</button>
</div>
</div>
</div>
)
}
return <Component {...args} />
},
}
Loading