Files
supabase/apps/studio/components/ui/BannerStack/BannerStackProvider.tsx
Danny White b5462ee7bb feat(www + studio): promote Select 2026 in www and Dashboard (#49511)
## What kind of change does this PR introduce?

Feature. Promotes Supabase Select 2026 across www and the Dashboard.

## What is the current behavior?

There is no active Select promotion on www or in the Dashboard.

## What is the new behavior?

- Adds a dismissible Select 2026 banner above the www navigation.
- Adds a low-priority Banner Stack card across hosted Studio project
pages.
- Shares a lightweight pixel lockup and CSS-only motion across both
surfaces, with light, dark, and reduced-motion treatments.
- Opens the application page in a new tab and automatically expires both
placements after October 2 in San Francisco.

## To test

- Open `/database` on the www preview. Confirm the banner is legible in
light and dark mode, the complete message remains visible at a phone
width, and the CTA opens `select.supabase.com` in a new tab.
- Dismiss the www banner, then refresh the page. Confirm it stays
dismissed.
- Open any `/project/{ref}` route in the Dashboard preview. Confirm the
Select card appears in the bottom-right Banner Stack behind
higher-priority notices.
- Dismiss the Dashboard card, navigate to another project page, and
confirm it stays dismissed.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a time-limited Select 2026 promotional banner across the website
and Studio.
* Added responsive themed artwork, animated visuals, campaign messaging,
and an external “Apply to attend” CTA.
  * Banner dismissal preferences are saved and respected across visits.
  * Promotion automatically disappears after the campaign ends.

* **Accessibility**
* Improved announcement dismissal controls with semantic buttons and
accessible labels.
  * Added reduced-motion support for promotional artwork.

* **Bug Fixes**
* Improved product-card loading effects to prevent hydration
inconsistencies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-25 18:07:38 +01:00

70 lines
2.3 KiB
TypeScript

import { createContext, useCallback, useContext, useState } from 'react'
export const BANNER_ID = {
DATABASE_CONNECTIONS: 'database-connections-banner',
INDEX_ADVISOR: 'index-advisor-banner',
TABLE_EDITOR_QUEUE_OPERATIONS: 'table-editor-queue-operations-banner',
RLS_EVENT_TRIGGER: 'rls-event-trigger-banner',
FREE_MICRO_UPGRADE: 'free-micro-upgrade-banner',
TOS_UPDATE: 'tos-update-banner',
LOGS_ALL_DEPRECATION: 'logs-all-deprecation-banner',
SELECT_26: 'select-2026-banner',
} as const
export type BannerId = (typeof BANNER_ID)[keyof typeof BANNER_ID]
export interface Banner {
id: BannerId
content: React.ReactNode
isDismissed: boolean
priority?: number
onDismiss?: () => void
}
interface BannerStackContextType {
banners: Banner[]
addBanner: (banner: Banner) => void
dismissBanner: (id: BannerId) => void
}
const BannerStackContext = createContext<BannerStackContextType | undefined>(undefined)
export const BannerStackProvider = ({ children }: { children: React.ReactNode }) => {
const [banners, setBanners] = useState<Banner[]>([])
const addBanner = useCallback((banner: Banner) => {
setBanners((prev) => {
const existingIndex = prev.findIndex((b) => b.id === banner.id)
if (existingIndex !== -1) {
if (!prev[existingIndex].isDismissed) return prev
const revived = [...prev]
revived[existingIndex] = banner
return revived.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
}
const newBanners = [...prev, banner]
return newBanners.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
})
}, [])
const dismissBanner = useCallback((id: string) => {
setBanners((prev) => prev.map((b) => (b.id === id ? { ...b, isDismissed: true } : b)))
setTimeout(() => {
// A later addBanner can revive this id before the exit animation
// finishes. Drop it only if it is still dismissed.
setBanners((prev) => prev.filter((b) => b.id !== id || !b.isDismissed))
}, 300)
}, [])
return (
<BannerStackContext.Provider value={{ banners, addBanner, dismissBanner }}>
{children}
</BannerStackContext.Provider>
)
}
export const useBannerStack = () => {
const context = useContext(BannerStackContext)
if (!context) throw new Error('useBannerStack must be used within BannerStackProvider')
return context
}