BannerStackProvider.tsx60 lines · main
1import { createContext, useCallback, useContext, useState } from 'react'
2
3export const BANNER_ID = {
4 METRICS_API: 'metrics-api-banner',
5 INDEX_ADVISOR: 'index-advisor-banner',
6 TABLE_EDITOR_QUEUE_OPERATIONS: 'table-editor-queue-operations-banner',
7 RLS_EVENT_TRIGGER: 'rls-event-trigger-banner',
8 RLS_TESTER: 'rls-tester-banner',
9 FREE_MICRO_UPGRADE: 'free-micro-upgrade-banner',
10} as const
11
12export type BannerId = (typeof BANNER_ID)[keyof typeof BANNER_ID]
13
14export interface Banner {
15 id: BannerId
16 content: React.ReactNode
17 isDismissed: boolean
18 priority?: number
19 onDismiss?: () => void
20}
21
22interface BannerStackContextType {
23 banners: Banner[]
24 addBanner: (banner: Banner) => void
25 dismissBanner: (id: BannerId) => void
26}
27
28const BannerStackContext = createContext<BannerStackContextType | undefined>(undefined)
29
30export const BannerStackProvider = ({ children }: { children: React.ReactNode }) => {
31 const [banners, setBanners] = useState<Banner[]>([])
32
33 const addBanner = useCallback((banner: Banner) => {
34 setBanners((prev) => {
35 const exists = prev.some((b) => b.id === banner.id)
36 if (exists) return prev
37 const newBanners = [...prev, banner]
38 return newBanners.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
39 })
40 }, [])
41
42 const dismissBanner = useCallback((id: string) => {
43 setBanners((prev) => prev.map((b) => (b.id === id ? { ...b, isDismissed: true } : b)))
44 setTimeout(() => {
45 setBanners((prev) => prev.filter((b) => b.id !== id))
46 }, 300)
47 }, [])
48
49 return (
50 <BannerStackContext.Provider value={{ banners, addBanner, dismissBanner }}>
51 {children}
52 </BannerStackContext.Provider>
53 )
54}
55
56export const useBannerStack = () => {
57 const context = useContext(BannerStackContext)
58 if (!context) throw new Error('useBannerStack must be used within BannerStackProvider')
59 return context
60}