diff --git a/components/analytics/charts.stories.tsx b/components/analytics/charts.stories.tsx new file mode 100644 index 0000000..a24e849 --- /dev/null +++ b/components/analytics/charts.stories.tsx @@ -0,0 +1,126 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { AnalyticsCharts, ChartSkeleton } from './charts' + +type AnalyticsChartsProps = React.ComponentProps + +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 + +export default meta +type Story = StoryObj + +/** 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: () => , +} diff --git a/components/error-boundary/section-error-boundary.stories.tsx b/components/error-boundary/section-error-boundary.stories.tsx new file mode 100644 index 0000000..d6f0833 --- /dev/null +++ b/components/error-boundary/section-error-boundary.stories.tsx @@ -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 + +export default meta +type Story = StoryObj + +/** Normal child renders without error. */ +export const Default: Story = { + args: { + children: ( +
+

This section rendered successfully.

+
+ ), + sectionName: 'Stream list', + }, +} + +/** Child component that throws an error. */ +export const WithError: Story = { + args: { + children: ( + + ), + sectionName: 'Analytics panel', + onReloadData: fn(), + }, +} + +/** Multiple sections, one with an error and one working. */ +export const MultipleWithErrorBoundary: Story = { + render: () => ( +
+ +
+

This section is working normally.

+
+
+ + + + + + +
+

This section is also working fine.

+
+
+
+ ), +} + +function ErrorThrowingChild() { + throw new Error('Something went wrong in this section') +} diff --git a/components/notifications/notification-preferences.stories.tsx b/components/notifications/notification-preferences.stories.tsx new file mode 100644 index 0000000..682de68 --- /dev/null +++ b/components/notifications/notification-preferences.stories.tsx @@ -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 + +export default meta +type Story = StoryObj + +/** 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(), + })) + }, +} diff --git a/components/settings/clear-local-data.stories.tsx b/components/settings/clear-local-data.stories.tsx new file mode 100644 index 0000000..ad154d3 --- /dev/null +++ b/components/settings/clear-local-data.stories.tsx @@ -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 + +export default meta +type Story = StoryObj + +/** 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) => { + const [confirming, setConfirming] = React.useState(true) + + // Render by intercepting the component and forcing the confirming state + return ( +
+
+
+ + + +
+

{mockSettingsCopy.clearLocalData.title}

+

+ {mockSettingsCopy.clearLocalData.description} +

+
+
+
+ + {mockSettingsCopy.clearLocalData.confirmPrompt} + + + +
+
+
+ ) + } + return + }, +}