Skip to content

Commit 133d20a

Browse files
committed
Add alerts component
1 parent 985379d commit 133d20a

File tree

4 files changed

+139
-0
lines changed

4 files changed

+139
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
at2.yaml
22
data.db
3+
data.db-journal
34
temp-at

at2-web/src/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { ModeToggle } from "./components/ModeToggle";
2323
import { LanguageToggle } from "./components/LanguageToggle";
2424
import { Button } from "./components/ui/button";
2525
import { User as UserIcon } from "lucide-react";
26+
import { Alerts } from "./components/Alerts";
2627

2728
export function App() {
2829
return (
@@ -178,6 +179,7 @@ const AppContent: FC = () => {
178179
<UserControls />
179180
</div>
180181
</header>
182+
<Alerts />
181183
<main className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 px-4 pb-10">
182184
{rooms.length === 0 && (
183185
<div className="col-span-full text-center py-10 text-neutral-600">

at2-web/src/components/Alerts.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useEffect, useState } from "react";
2+
import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
3+
import { Info, TriangleAlert } from "lucide-react";
4+
5+
interface FlashAlert {
6+
type: "warning" | "info" | "error";
7+
title: string;
8+
body: string;
9+
isCode?: boolean;
10+
}
11+
12+
export function Alerts() {
13+
const [alerts, setAlerts] = useState<FlashAlert[]>([]);
14+
15+
useEffect(() => {
16+
const cookies = document.cookie.split(";");
17+
const flashAlertCookie = cookies.find((cookie) =>
18+
cookie.trim().startsWith("at2_flash_alert=")
19+
);
20+
21+
if (flashAlertCookie) {
22+
try {
23+
const encodedValue = flashAlertCookie.split("=")[1];
24+
const decodedValue = decodeURIComponent(encodedValue);
25+
const alertData: FlashAlert = JSON.parse(decodedValue);
26+
setAlerts((prev) => [...prev, alertData]);
27+
28+
// Clear the cookie
29+
document.cookie =
30+
"at2_flash_alert=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
31+
} catch (error) {
32+
console.error("Failed to parse flash alert cookie:", error);
33+
}
34+
}
35+
}, []);
36+
37+
if (alerts.length === 0) return null;
38+
39+
return (
40+
<div className="flex flex-col gap-2 px-4 pb-4">
41+
{alerts.map((alert, index) => {
42+
let variant: "default" | "destructive" = "default";
43+
let Icon = Info;
44+
45+
if (alert.type === "error") {
46+
variant = "destructive";
47+
Icon = TriangleAlert;
48+
} else if (alert.type === "warning") {
49+
Icon = TriangleAlert;
50+
}
51+
52+
return (
53+
<Alert key={index} variant={variant}>
54+
<Icon className="h-4 w-4" />
55+
<AlertTitle>{alert.title}</AlertTitle>
56+
<AlertDescription>
57+
{alert.isCode ? (
58+
<pre className="font-mono text-xs mt-2 p-2 bg-muted rounded overflow-auto">
59+
{alert.body}
60+
</pre>
61+
) : (
62+
alert.body
63+
)}
64+
</AlertDescription>
65+
</Alert>
66+
);
67+
})}
68+
</div>
69+
);
70+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as React from "react"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
4+
import { cn } from "@/lib/utils"
5+
6+
const alertVariants = cva(
7+
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8+
{
9+
variants: {
10+
variant: {
11+
default: "bg-card text-card-foreground",
12+
destructive:
13+
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14+
},
15+
},
16+
defaultVariants: {
17+
variant: "default",
18+
},
19+
}
20+
)
21+
22+
function Alert({
23+
className,
24+
variant,
25+
...props
26+
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27+
return (
28+
<div
29+
data-slot="alert"
30+
role="alert"
31+
className={cn(alertVariants({ variant }), className)}
32+
{...props}
33+
/>
34+
)
35+
}
36+
37+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38+
return (
39+
<div
40+
data-slot="alert-title"
41+
className={cn(
42+
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43+
className
44+
)}
45+
{...props}
46+
/>
47+
)
48+
}
49+
50+
function AlertDescription({
51+
className,
52+
...props
53+
}: React.ComponentProps<"div">) {
54+
return (
55+
<div
56+
data-slot="alert-description"
57+
className={cn(
58+
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59+
className
60+
)}
61+
{...props}
62+
/>
63+
)
64+
}
65+
66+
export { Alert, AlertTitle, AlertDescription }

0 commit comments

Comments
 (0)