Skip to content

Commit 9fc692a

Browse files
committed
global alerts
1 parent 1616795 commit 9fc692a

4 files changed

Lines changed: 180 additions & 1 deletion

File tree

app/global-alert-rules.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Global Alert Rules
2+
3+
- Use the global alert system for cross-page success, warning, error, and info messages.
4+
- Access it via the `useAlert` hook from `src/contexts/AlertContext.tsx`.
5+
- Call `showAlert({ type, title?, message, durationMs? })` to display an alert.
6+
- Supported `type` values: `success`, `warning`, `error`, `info`.
7+
- Alerts appear in the top-right corner with a blurred background shader.
8+
- Prefer short, action-focused messages and optional concise titles.
9+
- Avoid using alerts for long-form content; use dialogs or dedicated pages instead.

app/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { GlobalShaderOverlay } from "@/components/ui/global-shader-overlay"
33
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
44
import { AuthProvider, useAuth } from './contexts/AuthContext';
55
import { GlobalProvider } from './contexts/GlobalContext';
6+
import { AlertProvider } from './contexts/AlertContext';
67
import { WorkspaceProvider } from './contexts/WorkspaceContext';
78
import { ErrorBoundary } from './components/ui/error-boundary';
89
import LandingPage from './pages/LandingPage';
@@ -54,7 +55,8 @@ function App() {
5455
<AuthProvider>
5556
<WorkspaceProvider>
5657
<GlobalProvider>
57-
<PWAProvider>
58+
<AlertProvider>
59+
<PWAProvider>
5860
<GlobalShaderOverlay />
5961
<Routes>
6062
<Route path="/" element={<LandingPage />} />
@@ -237,6 +239,7 @@ function App() {
237239
</Routes>
238240
<PWAPrompt />
239241
</PWAProvider>
242+
</AlertProvider>
240243
</GlobalProvider>
241244
</WorkspaceProvider>
242245
</AuthProvider>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React from "react"
2+
import { CheckCircle2, AlertTriangle, Info, XCircle, X } from "lucide-react"
3+
import { cn } from "@/lib/utils"
4+
5+
type AlertType = "success" | "warning" | "error" | "info"
6+
7+
type GlobalAlertProps = {
8+
type: AlertType
9+
title?: string
10+
message: string
11+
onClose?: () => void
12+
}
13+
14+
const typeStyles: Record<AlertType, string> = {
15+
success:
16+
"border-emerald-500/40 bg-emerald-900/70 text-emerald-50 shadow-emerald-500/30",
17+
warning:
18+
"border-amber-400/50 bg-amber-900/80 text-amber-50 shadow-amber-400/30",
19+
error:
20+
"border-rose-500/50 bg-rose-950/80 text-rose-50 shadow-rose-500/40",
21+
info:
22+
"border-sky-500/50 bg-sky-950/80 text-sky-50 shadow-sky-500/40",
23+
}
24+
25+
const typeIcon: Record<AlertType, React.ComponentType<{ className?: string }>> =
26+
{
27+
success: CheckCircle2,
28+
warning: AlertTriangle,
29+
error: XCircle,
30+
info: Info,
31+
}
32+
33+
export const GlobalAlert = ({
34+
type,
35+
title,
36+
message,
37+
onClose,
38+
}: GlobalAlertProps) => {
39+
const Icon = typeIcon[type]
40+
41+
return (
42+
<div className="pointer-events-auto fixed top-4 right-4 z-40 flex max-w-sm flex-col gap-3">
43+
<div
44+
className={cn(
45+
"relative overflow-hidden rounded-2xl border px-4 py-3 shadow-2xl backdrop-blur-xl",
46+
"bg-background/80 dark:bg-slate-950/80",
47+
"transition-transform transition-opacity duration-200 ease-out",
48+
typeStyles[type]
49+
)}
50+
role="status"
51+
aria-live="polite"
52+
>
53+
<div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-white/5 via-white/0 to-white/10" />
54+
<div className="pointer-events-none absolute -inset-x-24 -top-24 h-40 bg-gradient-to-br from-white/10 via-transparent to-white/0 blur-2xl" />
55+
<div className="relative flex items-start gap-3">
56+
<div className="mt-0.5 flex h-6 w-6 items-center justify-center rounded-full bg-black/20">
57+
<Icon className="h-4 w-4" />
58+
</div>
59+
<div className="flex-1">
60+
{title && (
61+
<div className="text-sm font-semibold leading-snug">
62+
{title}
63+
</div>
64+
)}
65+
<div className="text-sm leading-snug text-white/90 dark:text-slate-100">
66+
{message}
67+
</div>
68+
</div>
69+
{onClose && (
70+
<button
71+
type="button"
72+
onClick={onClose}
73+
className="ml-2 inline-flex h-6 w-6 items-center justify-center rounded-full border border-white/20 bg-black/20 text-white/80 transition hover:bg-black/40 focus:outline-none focus:ring-2 focus:ring-white/60 focus:ring-offset-2 focus:ring-offset-slate-950"
74+
aria-label="Dismiss alert"
75+
>
76+
<X className="h-3 w-3" />
77+
</button>
78+
)}
79+
</div>
80+
</div>
81+
</div>
82+
)
83+
}
84+

app/src/contexts/AlertContext.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, {
2+
createContext,
3+
useContext,
4+
useState,
5+
useRef,
6+
ReactNode,
7+
} from "react"
8+
import { GlobalAlert } from "@/components/ui/global-alert"
9+
10+
type AlertType = "success" | "warning" | "error" | "info"
11+
12+
type AlertOptions = {
13+
type: AlertType
14+
title?: string
15+
message: string
16+
durationMs?: number
17+
}
18+
19+
type AlertState = {
20+
type: AlertType
21+
title?: string
22+
message: string
23+
}
24+
25+
type AlertContextValue = {
26+
showAlert: (options: AlertOptions) => void
27+
hideAlert: () => void
28+
}
29+
30+
const AlertContext = createContext<AlertContextValue | undefined>(undefined)
31+
32+
export const useAlert = () => {
33+
const context = useContext(AlertContext)
34+
if (!context) {
35+
throw new Error("useAlert must be used within an AlertProvider")
36+
}
37+
return context
38+
}
39+
40+
export const AlertProvider = ({ children }: { children: ReactNode }) => {
41+
const [alert, setAlert] = useState<AlertState | null>(null)
42+
const timeoutRef = useRef<number | undefined>(undefined)
43+
44+
const clearTimer = () => {
45+
if (timeoutRef.current !== undefined) {
46+
window.clearTimeout(timeoutRef.current)
47+
timeoutRef.current = undefined
48+
}
49+
}
50+
51+
const hideAlert = () => {
52+
clearTimer()
53+
setAlert(null)
54+
}
55+
56+
const showAlert = ({
57+
type,
58+
title,
59+
message,
60+
durationMs = 5000,
61+
}: AlertOptions) => {
62+
clearTimer()
63+
setAlert({ type, title, message })
64+
timeoutRef.current = window.setTimeout(() => {
65+
hideAlert()
66+
}, durationMs)
67+
}
68+
69+
return (
70+
<AlertContext.Provider value={{ showAlert, hideAlert }}>
71+
{children}
72+
{alert && (
73+
<GlobalAlert
74+
type={alert.type}
75+
title={alert.title}
76+
message={alert.message}
77+
onClose={hideAlert}
78+
/>
79+
)}
80+
</AlertContext.Provider>
81+
)
82+
}
83+

0 commit comments

Comments
 (0)