Skip to content
Draft
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
20 changes: 16 additions & 4 deletions packages/curve-ui-kit/src/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import lodash from 'lodash'
import { useMemo } from 'react'
import { useCallback, useMemo } from 'react'
import type { Address } from '@curvefi/prices-api'
import type { VisibilityVariants } from '@ui-kit/shared/ui/DataTable/visibility.types'
import { defaultReleaseChannel, ReleaseChannel } from '@ui-kit/utils'
import { type MigrationOptions, useStoredState } from './useStoredState'

const { kebabCase } = lodash

// old keys that are not used anymore - clean them up
window.localStorage.removeItem('phishing-warning-dismissed')

function getFromLocalStorage<T>(storageKey: string): T | null {
if (typeof window === 'undefined') {
return null
Expand Down Expand Up @@ -72,3 +69,18 @@ export const useFavoriteMarkets = () => {
const initialValue = useMemo(() => [], [])
return useLocalStorage<Address[]>('favoriteMarkets', initialValue)
}

export const useDismissBanner = (bannerKey: string, expirationTime: number) => {
const [dismissedAt, setDismissedAt] = useLocalStorage<number | null>(bannerKey, null)

const shouldShowBanner = useMemo(
() => dismissedAt == null || Date.now() - dismissedAt >= expirationTime, // Show if dismissed more than the expiration time
[dismissedAt, expirationTime],
)

const dismissBanner = useCallback(() => {
setDismissedAt(Date.now())
}, [setDismissedAt])

return { shouldShowBanner, dismissBanner }
}
106 changes: 52 additions & 54 deletions packages/curve-ui-kit/src/shared/ui/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,112 +6,110 @@ import Card from '@mui/material/Card'
import IconButton from '@mui/material/IconButton'
import LinkMui from '@mui/material/Link'
import Stack from '@mui/material/Stack'
import Typography, { type TypographyProps } from '@mui/material/Typography'
import Typography from '@mui/material/Typography'
import { t } from '@ui-kit/lib/i18n'
import { ArrowTopRightIcon } from '@ui-kit/shared/icons/ArrowTopRightIcon'
import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
import { ChangeTheme, InvertTheme } from './ThemeProvider'

type BannerSeverity = 'default' | 'highlight' | 'warning' | 'alert'
type BannerSeverity = 'info' | 'highlight' | 'warning' | 'alert'

const WrapperSx: Record<BannerSeverity, SxProps<Theme>> = {
default: {
border: (t) => `1px solid ${t.design.Layer.Highlight.Outline}`,
backgroundColor: (t) => t.design.Layer[1].Fill,
const BannerSx: Record<BannerSeverity, { title: SxProps<Theme>; subtitle: SxProps<Theme>; wrapper: SxProps<Theme> }> = {
info: {
title: { color: (t) => t.design.Text.TextColors.FilledFeedback.Info.Primary },
subtitle: { color: (t) => t.design.Text.TextColors.FilledFeedback.Info.Secondary },
wrapper: {
border: (t) => `1px solid ${t.design.Layer.Highlight.Outline}`,
backgroundColor: (t) => t.design.Layer[1].Fill,
},
},
alert: {
title: { color: (t) => t.design.Text.TextColors.FilledFeedback.Alert.Primary },
subtitle: { color: (t) => t.design.Text.TextColors.FilledFeedback.Alert.Secondary },
wrapper: { backgroundColor: (t) => t.design.Layer.Feedback.Error },
},
warning: {
title: { color: (t) => t.design.Text.TextColors.FilledFeedback.Warning.Primary },
subtitle: { color: (t) => t.design.Text.TextColors.FilledFeedback.Warning.Secondary },
wrapper: { backgroundColor: (t) => t.design.Layer.Feedback.Warning },
},
highlight: {
border: (t) => `1px solid ${t.design.Layer.Highlight.Outline}`,
backgroundColor: (t) => t.design.Color.Primary[800],
title: { color: (t) => t.design.Text.TextColors.FilledFeedback.Highlight.Primary },
subtitle: { color: (t) => t.design.Text.TextColors.FilledFeedback.Highlight.Secondary },
wrapper: { backgroundColor: (t) => t.design.Layer.Feedback.Info },
},
alert: { backgroundColor: (t) => t.design.Layer.Feedback.Error },
warning: { backgroundColor: (t) => t.design.Layer.Feedback.Warning },
}

const TitleColor: Record<BannerSeverity, TypographyProps['color']> = {
default: 'textHighlight',
alert: 'textPrimary',
warning: 'textPrimary',
highlight: 'textPrimary',
}
const { MaxWidth, Spacing } = SizesAndSpaces

const TitleInverted: Record<BannerSeverity, boolean> = {
default: false,
alert: true,
warning: false,
highlight: true,
export type BannerProps = {
onClick?: () => void
buttonText?: string
children: ReactNode
severity?: BannerSeverity
learnMoreUrl?: string
subtitle?: ReactNode
testId?: string
}

const { MaxWidth, Spacing } = SizesAndSpaces

/**
* Banner message component used to display important information with different severity levels.
* This is not complete yet: it doesn't support a subtitle or a close button from the design system.
*/
export const Banner = ({
onClick,
buttonText,
children,
severity = 'default',
severity = 'info',
learnMoreUrl,
color,
}: {
onClick?: () => void
buttonText?: string
children: ReactNode
severity?: BannerSeverity
learnMoreUrl?: string
color?: TypographyProps['color']
}) => (
subtitle,
testId,
}: BannerProps) => (
<Card
sx={{
display: 'flex',
gap: Spacing.md,
alignSelf: 'stretch',
paddingInline: Spacing.md,
paddingBlock: Spacing.xs,
alignItems: 'center',
justifyContent: 'center',
...WrapperSx[severity],
...BannerSx[severity].wrapper,
}}
data-testid={testId}
>
<Stack
direction="row"
sx={{ width: '100%', maxWidth: MaxWidth.banner }}
alignItems="center"
justifyContent="space-between"
>
<InvertTheme inverted={TitleInverted[severity]}>
<Typography color={color ?? TitleColor[severity]} variant="headingXsBold">
<Stack direction="column" width="100%" maxWidth={MaxWidth.banner}>
<Stack direction="row" alignItems="center" justifyContent="space-between" gap={Spacing.sm}>
<Typography sx={{ ...BannerSx[severity].title }} variant="headingXsBold">
{children}
</Typography>
</InvertTheme>
<Stack direction="row" alignItems="center" justifyContent="start" height="100%">
{/* fixme: currently using light theme on dark theme */}
<ChangeTheme to={color === '#000' && 'light'}>
<Stack direction="row" alignItems="center" justifyContent="start" height="100%">
{learnMoreUrl && (
<Button
component={LinkMui}
href={learnMoreUrl}
target="_blank"
color="ghost"
variant="link"
endIcon={<ArrowTopRightIcon />}
endIcon={<ArrowTopRightIcon fontSize="small" />}
size="extraSmall"
sx={{ ...BannerSx[severity].title }}
>
{t`Learn more`}
</Button>
)}
{onClick &&
(buttonText ? (
<Button color="ghost" onClick={onClick} size="extraSmall">
<Button color="ghost" onClick={onClick} size="extraSmall" sx={{ ...BannerSx[severity].title }}>
{buttonText}
</Button>
) : (
<IconButton onClick={onClick} size="extraSmall">
<IconButton onClick={onClick} size="extraSmall" sx={{ ...BannerSx[severity].title }}>
<CloseIcon />
</IconButton>
))}
</ChangeTheme>
</Stack>
</Stack>
<Stack direction="row" alignItems="center" justifyContent="start" height="100%">
<Typography sx={{ ...BannerSx[severity].subtitle }} variant="bodySRegular">
{subtitle}
</Typography>
</Stack>
</Stack>
</Card>
Expand Down
11 changes: 3 additions & 8 deletions packages/curve-ui-kit/src/shared/ui/GlobalBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useChainId, useConnection, useSwitchChain } from 'wagmi'
import Box from '@mui/material/Box'
import { useTheme } from '@mui/material/styles'
import { isFailure, useCurve, type WagmiChainId } from '@ui-kit/features/connect-wallet'
import { useReleaseChannel } from '@ui-kit/hooks/useLocalStorage'
import { t } from '@ui-kit/lib/i18n'
import { LlamaIcon } from '@ui-kit/shared/icons/LlamaIcon'
import { Banner } from '@ui-kit/shared/ui/Banner'
import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
import { isCypress, ReleaseChannel } from '@ui-kit/utils'
import { PhishingWarningBanner } from '@ui-kit/widgets/Header/PhishingWarningBanner'

export type GlobalBannerProps = {
networkId: string
Expand All @@ -27,23 +27,18 @@ export const GlobalBanner = ({ networkId, chainId }: GlobalBannerProps) => {
const walletChainId = useChainId()
const showSwitchNetworkMessage = isConnected && chainId && walletChainId != chainId
const showConnectApiErrorMessage = !showSwitchNetworkMessage && isFailure(connectState)
const warnColor = useTheme().palette.mode === 'dark' ? '#000' : 'textSecondary' // todo: fix this in the design system of the alert component
return (
<Box>
<PhishingWarningBanner />
{releaseChannel !== ReleaseChannel.Stable && !isCypress && (
<Banner onClick={() => setReleaseChannel(ReleaseChannel.Stable)} buttonText={t`Disable ${releaseChannel} Mode`}>
<LlamaIcon sx={{ width: IconSize.sm, height: IconSize.sm }} /> {t`${releaseChannel} Mode Enabled`}
</Banner>
)}
{maintenanceMessage && (
<Banner severity="warning" color={warnColor}>
{maintenanceMessage}
</Banner>
)}
{maintenanceMessage && <Banner severity="warning">{maintenanceMessage}</Banner>}
{showSwitchNetworkMessage && (
<Banner
severity="warning"
color={warnColor}
buttonText={t`Change network`}
onClick={() => switchChain({ chainId: chainId as WagmiChainId })}
>
Expand Down
66 changes: 37 additions & 29 deletions packages/curve-ui-kit/src/shared/ui/stories/Banner.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { LlamaIcon } from '@ui-kit/shared/icons/LlamaIcon'
import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces'
import { Banner } from '../Banner'

const { IconSize } = SizesAndSpaces

const meta: Meta<typeof Banner> = {
title: 'UI Kit/Primitives/Banner',
component: (props) => (
Expand All @@ -24,9 +26,13 @@ const meta: Meta<typeof Banner> = {
argTypes: {
severity: {
control: 'select',
options: ['error', 'warning', 'info'],
options: ['error', 'warning', 'info', 'highlight'],
description: 'The severity level of the banner message',
},
learnMoreUrl: {
control: 'text',
description: 'The URL to navigate to when clicking the learn more button',
},
buttonText: {
control: 'text',
description: 'Text for the action button (optional)',
Expand All @@ -35,37 +41,42 @@ const meta: Meta<typeof Banner> = {
action: 'clicked',
description: 'Function called when the button is clicked',
},
subtitle: {
control: 'text',
description: 'Subtitle for the banner message (optional)',
},
},
}

export default meta
type Story = StoryObj<typeof Banner>

export const Error: Story = {
export const Info: Story = {
args: {
severity: 'alert',
children: 'This is an alert message',
severity: 'info',
children: 'This is a default message',
subtitle: 'This is a subtitle for the default message',
},
}

export const Warning: Story = {
export const Highlight: Story = {
args: {
severity: 'warning',
children: 'This is a warning message',
severity: 'highlight',
children: 'This is a highlight message',
subtitle: 'This is a subtitle for the highlight message',
},
}

export const Info: Story = {
export const Warning: Story = {
args: {
severity: 'default',
children: 'This is a default message',
severity: 'warning',
children: 'This is a warning message',
subtitle: 'This is a subtitle for the warning message',
},
}

export const Highlight: Story = {
export const Error: Story = {
args: {
severity: 'highlight',
children: 'This is a highlight message',
severity: 'alert',
children: 'This is an alert message',
subtitle: 'This is a subtitle for the alert message',
},
}

Expand All @@ -77,8 +88,13 @@ export const WithButton: Story = {
onClick: fn(),
},
}

const { IconSize } = SizesAndSpaces
export const WithLearnMoreUrl: Story = {
args: {
severity: 'alert',
children: 'This is an error message with a learn more URL',
learnMoreUrl: 'https://www.curve.finance',
},
}

export const WithIcon: Story = {
args: {
Expand All @@ -91,24 +107,16 @@ export const WithIcon: Story = {
},
}

export const NetworkSwitchExample: Story = {
args: {
severity: 'alert',
buttonText: 'Change network',
children: "Please switch your wallet's network to Ethereum to use Curve on Ethereum.",
},
}

export const MaintenanceExample: Story = {
args: {
severity: 'warning',
children: 'Scheduled maintenance in progress. Some features may be temporarily unavailable.',
},
}

export const ApiErrorExample: Story = {
export const LongErrorExample: Story = {
args: {
severity: 'alert',
children: 'There is an issue connecting to the API. Please try to switch your RPC in your wallet settings.',
children:
'There is an issue connecting to the API. Please try to switch your RPC in your wallet settings. There is an issue connecting to the API. Please try to switch your RPC in your wallet settings.',
},
}
Loading
Loading