Openstatus www.openstatus.dev

Style/toast (#408)

* style: add warning variant, and icons for all variants

* fix: don't allow test request if previous is pending

* chore: use theme color

---------

Co-authored-by: mxkaske <maximilian@kaske.org>

authored by

Arshdeep K
mxkaske
and committed by
GitHub
d1ec46d5 e7681f42

+77 -13
+3
.gitignore
··· 54 54 openstatus-test.db 55 55 openstatus-test.db-shm 56 56 openstatus-test.db-wal 57 + 58 + #webstorm 59 + .idea
+9
apps/web/src/components/forms/monitor-form.tsx
··· 186 186 }; 187 187 188 188 const sendTestPing = () => { 189 + if (isTestPending) { 190 + return; 191 + } 192 + const { url } = form.getValues(); 193 + if (!url) { 194 + toast("test-warning-empty-url"); 195 + return; 196 + } 197 + 189 198 startTestTransition(async () => { 190 199 const isSuccessful = await pingEndpoint(); 191 200 if (isSuccessful) {
+6
apps/web/src/hooks/use-toast-action.tsx
··· 17 17 "unique-slug": { 18 18 title: "Slug is already taken", 19 19 description: "Please select another slug. Every slug is unique.", 20 + variant: "warning", 20 21 }, 21 22 success: { title: "Success" }, 22 23 deleted: { title: "Deleted successfully" }, // TODO: we are not informing the user besides the visual changes when an entry has been deleted ··· 25 26 title: "Connection Failed", 26 27 // description: "Be sure to include the auth headers.", 27 28 variant: "destructive", 29 + }, 30 + "test-warning-empty-url": { 31 + title: "URL is Empty", 32 + description: "Please enter a valid, non-empty URL", 33 + variant: "warning", 28 34 }, 29 35 "test-success": { 30 36 title: "Connection Established",
+47 -5
packages/ui/src/components/toast.tsx
··· 1 1 "use client"; 2 2 3 3 import * as React from "react"; 4 + import { useMemo } from "react"; 4 5 import * as ToastPrimitives from "@radix-ui/react-toast"; 5 6 import { cva } from "class-variance-authority"; 6 7 import type { VariantProps } from "class-variance-authority"; 7 - import { X } from "lucide-react"; 8 + import { Check, ShieldAlert, X } from "lucide-react"; 8 9 9 10 import { cn } from "../lib/utils"; 10 11 ··· 26 27 ToastViewport.displayName = ToastPrimitives.Viewport.displayName; 27 28 28 29 const toastVariants = cva( 29 - "data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full", 30 + "data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full bg-background", 30 31 { 31 32 variants: { 32 33 variant: { 33 - default: "bg-background border", 34 - destructive: 35 - "group destructive border-destructive bg-destructive text-destructive-foreground", 34 + // REMINDER: same keys as toastIconVariant 35 + default: "", 36 + destructive: "", 37 + warning: "", 36 38 }, 37 39 }, 38 40 defaultVariants: { ··· 41 43 }, 42 44 ); 43 45 46 + const toastIconVariant = cva("", { 47 + variants: { 48 + variant: { 49 + default: "bg-green-500", 50 + destructive: "bg-destructive", 51 + warning: "bg-amber-500", 52 + }, 53 + }, 54 + defaultVariants: { 55 + variant: "default", 56 + }, 57 + }); 58 + 44 59 const Toast = React.forwardRef< 45 60 React.ElementRef<typeof ToastPrimitives.Root>, 46 61 React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & ··· 56 71 }); 57 72 Toast.displayName = ToastPrimitives.Root.displayName; 58 73 74 + const ToastIcon = ({ variant }: VariantProps<typeof toastIconVariant>) => { 75 + const Icon = useMemo(() => { 76 + switch (variant) { 77 + case "destructive": 78 + return X; 79 + 80 + case "warning": 81 + return ShieldAlert; 82 + 83 + default: 84 + return Check; 85 + } 86 + }, [variant]); 87 + 88 + return ( 89 + <div 90 + className={cn( 91 + "h-fit w-fit rounded-full p-2", 92 + toastIconVariant({ variant }), 93 + )} 94 + > 95 + <Icon className="text-background h-4 w-4" /> 96 + </div> 97 + ); 98 + }; 99 + 59 100 const ToastAction = React.forwardRef< 60 101 React.ElementRef<typeof ToastPrimitives.Action>, 61 102 React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> ··· 127 168 ToastDescription, 128 169 ToastClose, 129 170 ToastAction, 171 + ToastIcon, 130 172 };
+12 -8
packages/ui/src/components/toaster.tsx
··· 4 4 Toast, 5 5 ToastClose, 6 6 ToastDescription, 7 + ToastIcon, 7 8 ToastProvider, 8 9 ToastTitle, 9 10 ToastViewport, ··· 17 18 <ToastProvider> 18 19 {toasts.map(function ({ id, title, description, action, ...props }) { 19 20 return ( 20 - <Toast key={id} {...props}> 21 - <div className="grid gap-1"> 22 - {title && <ToastTitle>{title}</ToastTitle>} 23 - {description && ( 24 - <ToastDescription>{description}</ToastDescription> 25 - )} 21 + <Toast key={id} {...props} duration={1000000}> 22 + <div className="flex w-fit items-center gap-3"> 23 + <ToastIcon variant={props.variant} /> 24 + <div className="grid gap-1"> 25 + {title && <ToastTitle>{title}</ToastTitle>} 26 + {description && ( 27 + <ToastDescription>{description}</ToastDescription> 28 + )} 29 + </div> 30 + {action} 31 + <ToastClose /> 26 32 </div> 27 - {action} 28 - <ToastClose /> 29 33 </Toast> 30 34 ); 31 35 })}