mirror of
https://github.com/supabase/supabase.git
synced 2026-06-15 08:05:21 +08:00
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
89 lines
2.4 KiB
TypeScript
89 lines
2.4 KiB
TypeScript
import { useParams } from 'common/hooks'
|
|
import { HTMLMotionProps, motion } from 'framer-motion'
|
|
import { X } from 'lucide-react'
|
|
import { ReactNode } from 'react'
|
|
import { Button, cn } from 'ui'
|
|
|
|
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
|
|
|
|
// Base props common to all feature banners
|
|
interface BaseFeatureBannerProps extends HTMLMotionProps<'div'> {
|
|
children: ReactNode
|
|
className?: string
|
|
dismissClassName?: string
|
|
defaultDismissed?: boolean
|
|
illustration?: ReactNode
|
|
bgAlt?: boolean
|
|
}
|
|
|
|
// Type for non-dismissable banners (no storageKey needed)
|
|
interface NonDismissableFeatureBannerProps extends BaseFeatureBannerProps {
|
|
dismissable?: false
|
|
storageKey?: never
|
|
}
|
|
|
|
// Type for dismissable banners (requires storageKey)
|
|
interface DismissableFeatureBannerProps extends BaseFeatureBannerProps {
|
|
dismissable: true
|
|
storageKey: string | ((ref: string) => string)
|
|
}
|
|
|
|
// Union type that enforces the constraint
|
|
export type FeatureBannerProps = NonDismissableFeatureBannerProps | DismissableFeatureBannerProps
|
|
|
|
export const FeatureBanner = ({
|
|
storageKey,
|
|
children,
|
|
className,
|
|
dismissClassName,
|
|
defaultDismissed = false,
|
|
illustration,
|
|
dismissable = false,
|
|
bgAlt = false,
|
|
...props
|
|
}: FeatureBannerProps) => {
|
|
const { ref } = useParams()
|
|
const key = storageKey && typeof storageKey === 'function' ? storageKey(ref ?? '') : storageKey
|
|
|
|
const [isDismissed, setIsDismissed] = useLocalStorageQuery(
|
|
key || 'feature-banner-dismissed',
|
|
defaultDismissed
|
|
)
|
|
|
|
if (dismissable && storageKey && isDismissed) return null
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 6 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{
|
|
type: 'spring',
|
|
stiffness: 500,
|
|
damping: 30,
|
|
mass: 1,
|
|
}}
|
|
{...props}
|
|
className={cn(
|
|
'pb-36 pt-10 relative w-full border xl:py-10 px-10 rounded-md overflow-hidden',
|
|
bgAlt && 'bg-background-alternative',
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
{illustration}
|
|
{dismissable && storageKey && (
|
|
<div className={cn('absolute top-3 right-3', dismissClassName)}>
|
|
<Button
|
|
type="text"
|
|
size="tiny"
|
|
icon={<X size={16} strokeWidth={1.5} />}
|
|
onClick={() => setIsDismissed(true)}
|
|
className="opacity-75 px-1"
|
|
aria-label="Dismiss notification"
|
|
/>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
)
|
|
}
|