Initial commit: DrinkTracker full-stack app

Next.js 14 drink collection tracker with AI-powered search,
menu scanning, ratings, wishlist, sharing, and CSV backup/restore.

Features:
- Auth (credentials + OAuth ready)
- Drink collection with ratings and reviews
- AI search via Claude/OpenAI with search history
- Menu photo scanning with AI extraction
- Wishlist / Try Later system
- Public sharing via slug URLs
- CSV backup and restore (merge/replace modes)
- Docker Compose for Postgres + MinIO + dev server

Security: docker-compose files use env var interpolation
instead of hardcoded secrets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
JP Scott
2026-03-01 12:27:08 -07:00
commit 969bc9347a
115 changed files with 19397 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { cva, type VariantProps } from "class-variance-authority"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,53 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,78 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,203 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { X } from "lucide-react"
interface DialogContextValue {
open: boolean
onOpenChange: (open: boolean) => void
}
const DialogContext = React.createContext<DialogContextValue>({
open: false,
onOpenChange: () => {},
})
interface DialogProps {
open?: boolean
onOpenChange?: (open: boolean) => void
children: React.ReactNode
}
function Dialog({ open = false, onOpenChange, children }: DialogProps) {
const [internalOpen, setInternalOpen] = React.useState(open)
const isControlled = onOpenChange !== undefined
const isOpen = isControlled ? open : internalOpen
const setOpen = isControlled ? onOpenChange : setInternalOpen
return (
<DialogContext.Provider value={{ open: isOpen, onOpenChange: setOpen }}>
{children}
</DialogContext.Provider>
)
}
interface DialogTriggerProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean
}
const DialogTrigger = React.forwardRef<HTMLButtonElement, DialogTriggerProps>(
({ className, onClick, ...props }, ref) => {
const { onOpenChange } = React.useContext(DialogContext)
return (
<button
ref={ref}
className={cn(className)}
onClick={(e) => {
onOpenChange(true)
onClick?.(e)
}}
{...props}
/>
)
}
)
DialogTrigger.displayName = "DialogTrigger"
interface DialogPortalProps {
children: React.ReactNode
}
function DialogPortal({ children }: DialogPortalProps) {
return <>{children}</>
}
const DialogOverlay = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { onOpenChange } = React.useContext(DialogContext)
return (
<div
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
onClick={() => onOpenChange(false)}
{...props}
/>
)
})
DialogOverlay.displayName = "DialogOverlay"
const DialogContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, children, ...props }, ref) => {
const { open, onOpenChange } = React.useContext(DialogContext)
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onOpenChange(false)
}
}
if (open) {
document.addEventListener("keydown", handleEscape)
document.body.style.overflow = "hidden"
}
return () => {
document.removeEventListener("keydown", handleEscape)
document.body.style.overflow = ""
}
}, [open, onOpenChange])
if (!open) return null
return (
<DialogPortal>
<DialogOverlay />
<div
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 sm:rounded-lg",
className
)}
{...props}
>
{children}
<button
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
</div>
</DialogPortal>
)
})
DialogContent.displayName = "DialogContent"
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h2
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = "DialogTitle"
const DialogDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = "DialogDescription"
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,211 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
// --- Dropdown Context ---
interface DropdownContextValue {
open: boolean
onOpenChange: (open: boolean) => void
}
const DropdownContext = React.createContext<DropdownContextValue>({
open: false,
onOpenChange: () => {},
})
// --- DropdownMenu ---
interface DropdownMenuProps {
open?: boolean
onOpenChange?: (open: boolean) => void
children: React.ReactNode
}
function DropdownMenu({
open: controlledOpen,
onOpenChange: controlledOnOpenChange,
children,
}: DropdownMenuProps) {
const [internalOpen, setInternalOpen] = React.useState(false)
const isControlled = controlledOnOpenChange !== undefined
const open = isControlled ? (controlledOpen ?? false) : internalOpen
const onOpenChange = isControlled ? controlledOnOpenChange : setInternalOpen
return (
<DropdownContext.Provider value={{ open, onOpenChange }}>
<div className="relative inline-block text-left">{children}</div>
</DropdownContext.Provider>
)
}
// --- DropdownMenuTrigger ---
interface DropdownMenuTriggerProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean
}
const DropdownMenuTrigger = React.forwardRef<
HTMLButtonElement,
DropdownMenuTriggerProps
>(({ className, onClick, ...props }, ref) => {
const { open, onOpenChange } = React.useContext(DropdownContext)
return (
<button
ref={ref}
className={cn(className)}
aria-expanded={open}
aria-haspopup="true"
onClick={(e) => {
onOpenChange(!open)
onClick?.(e)
}}
{...props}
/>
)
})
DropdownMenuTrigger.displayName = "DropdownMenuTrigger"
// --- DropdownMenuContent ---
interface DropdownMenuContentProps
extends React.HTMLAttributes<HTMLDivElement> {
align?: "start" | "center" | "end"
sideOffset?: number
}
const DropdownMenuContent = React.forwardRef<
HTMLDivElement,
DropdownMenuContentProps
>(({ className, align = "center", children, ...props }, ref) => {
const { open, onOpenChange } = React.useContext(DropdownContext)
const contentRef = React.useRef<HTMLDivElement>(null)
React.useEffect(() => {
if (!open) return
const handleClickOutside = (e: MouseEvent) => {
if (
contentRef.current &&
!contentRef.current.contains(e.target as Node) &&
!(e.target as HTMLElement).closest("[aria-haspopup]")
) {
onOpenChange(false)
}
}
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onOpenChange(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
document.addEventListener("keydown", handleEscape)
return () => {
document.removeEventListener("mousedown", handleClickOutside)
document.removeEventListener("keydown", handleEscape)
}
}, [open, onOpenChange])
if (!open) return null
return (
<div
ref={(node) => {
(contentRef as React.MutableRefObject<HTMLDivElement | null>).current =
node
if (typeof ref === "function") ref(node)
else if (ref) ref.current = node
}}
className={cn(
"absolute z-50 mt-1 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95",
align === "start" && "left-0",
align === "center" && "left-1/2 -translate-x-1/2",
align === "end" && "right-0",
className
)}
{...props}
>
{children}
</div>
)
})
DropdownMenuContent.displayName = "DropdownMenuContent"
// --- DropdownMenuItem ---
interface DropdownMenuItemProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
inset?: boolean
}
const DropdownMenuItem = React.forwardRef<
HTMLButtonElement,
DropdownMenuItemProps
>(({ className, inset, onClick, ...props }, ref) => {
const { onOpenChange } = React.useContext(DropdownContext)
return (
<button
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50",
inset && "pl-8",
className
)}
onClick={(e) => {
onClick?.(e)
onOpenChange(false)
}}
{...props}
/>
)
})
DropdownMenuItem.displayName = "DropdownMenuItem"
// --- DropdownMenuSeparator ---
const DropdownMenuSeparator = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = "DropdownMenuSeparator"
// --- DropdownMenuLabel ---
const DropdownMenuLabel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }
>(({ className, inset, ...props }, ref) => (
<div
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = "DropdownMenuLabel"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuLabel,
}

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { cva, type VariantProps } from "class-variance-authority"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
export interface LabelProps
extends React.LabelHTMLAttributes<HTMLLabelElement>,
VariantProps<typeof labelVariants> {}
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
({ className, ...props }, ref) => (
<label
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
)
)
Label.displayName = "Label"
export { Label }

View File

@@ -0,0 +1,49 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { ChevronDown } from "lucide-react"
export interface SelectProps
extends React.SelectHTMLAttributes<HTMLSelectElement> {}
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => {
return (
<div className="relative">
<select
className={cn(
"flex h-10 w-full appearance-none rounded-md border border-input bg-background px-3 py-2 pr-8 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
>
{children}
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-3 h-4 w-4 opacity-50" />
</div>
)
}
)
Select.displayName = "Select"
export interface SelectOptionProps
extends React.OptionHTMLAttributes<HTMLOptionElement> {}
const SelectOption = React.forwardRef<HTMLOptionElement, SelectOptionProps>(
({ className, ...props }, ref) => {
return <option ref={ref} className={cn(className)} {...props} />
}
)
SelectOption.displayName = "SelectOption"
export interface SelectGroupProps
extends React.OptgroupHTMLAttributes<HTMLOptGroupElement> {}
const SelectGroup = React.forwardRef<HTMLOptGroupElement, SelectGroupProps>(
({ className, ...props }, ref) => {
return <optgroup ref={ref} className={cn(className)} {...props} />
}
)
SelectGroup.displayName = "SelectGroup"
export { Select, SelectOption, SelectGroup }

View File

@@ -0,0 +1,29 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
orientation?: "horizontal" | "vertical"
decorative?: boolean
}
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<div
ref={ref}
role={decorative ? "none" : "separator"}
aria-orientation={decorative ? undefined : orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = "Separator"
export { Separator }

View File

@@ -0,0 +1,16 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@@ -0,0 +1,23 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

232
src/components/ui/toast.tsx Normal file
View File

@@ -0,0 +1,232 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
success:
"border-green-500 bg-green-50 text-green-900 dark:bg-green-950 dark:text-green-100",
},
},
defaultVariants: {
variant: "default",
},
}
)
// --- Toast Types ---
type ToastVariant = VariantProps<typeof toastVariants>["variant"]
interface ToastMessage {
id: string
title?: string
description?: string
variant?: ToastVariant
duration?: number
}
interface ToastState {
toasts: ToastMessage[]
}
type ToastAction =
| { type: "ADD_TOAST"; toast: ToastMessage }
| { type: "REMOVE_TOAST"; id: string }
// --- Toast Reducer ---
function toastReducer(state: ToastState, action: ToastAction): ToastState {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [...state.toasts, action.toast],
}
case "REMOVE_TOAST":
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.id),
}
default:
return state
}
}
// --- Toast Context ---
interface ToastContextValue {
toasts: ToastMessage[]
toast: (props: Omit<ToastMessage, "id">) => void
dismiss: (id: string) => void
}
const ToastContext = React.createContext<ToastContextValue | undefined>(
undefined
)
let toastCount = 0
function genId() {
toastCount = (toastCount + 1) % Number.MAX_SAFE_INTEGER
return toastCount.toString()
}
// --- Toast Provider ---
interface ToastProviderProps {
children: React.ReactNode
}
function ToastProvider({ children }: ToastProviderProps) {
const [state, dispatch] = React.useReducer(toastReducer, { toasts: [] })
const toast = React.useCallback(
(props: Omit<ToastMessage, "id">) => {
const id = genId()
const duration = props.duration ?? 5000
dispatch({ type: "ADD_TOAST", toast: { ...props, id } })
if (duration > 0) {
setTimeout(() => {
dispatch({ type: "REMOVE_TOAST", id })
}, duration)
}
},
[]
)
const dismiss = React.useCallback((id: string) => {
dispatch({ type: "REMOVE_TOAST", id })
}, [])
return (
<ToastContext.Provider value={{ toasts: state.toasts, toast, dismiss }}>
{children}
<ToastViewport toasts={state.toasts} dismiss={dismiss} />
</ToastContext.Provider>
)
}
// --- useToast Hook ---
function useToast() {
const context = React.useContext(ToastContext)
if (!context) {
throw new Error("useToast must be used within a ToastProvider")
}
return context
}
// --- Toast UI Components ---
interface ToastViewportProps {
toasts: ToastMessage[]
dismiss: (id: string) => void
}
function ToastViewport({ toasts, dismiss }: ToastViewportProps) {
if (toasts.length === 0) return null
return (
<div className="fixed bottom-0 right-0 z-[100] flex max-h-screen w-full flex-col-reverse gap-2 p-4 sm:max-w-[420px]">
{toasts.map((t) => (
<Toast key={t.id} variant={t.variant}>
<div className="grid gap-1">
{t.title && <ToastTitle>{t.title}</ToastTitle>}
{t.description && (
<ToastDescription>{t.description}</ToastDescription>
)}
</div>
<ToastClose onClick={() => dismiss(t.id)} />
</Toast>
))}
</div>
)
}
// --- Toast Primitives ---
interface ToastProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof toastVariants> {}
const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
({ className, variant, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
}
)
Toast.displayName = "Toast"
const ToastTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = "ToastTitle"
const ToastDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = "ToastDescription"
interface ToastCloseProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
const ToastClose = React.forwardRef<HTMLButtonElement, ToastCloseProps>(
({ className, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100",
className
)}
{...props}
>
<X className="h-4 w-4" />
</button>
)
}
)
ToastClose.displayName = "ToastClose"
export {
ToastProvider,
useToast,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
toastVariants,
}
export type { ToastMessage, ToastVariant }