1
0
Fork 0

chore(ui): add shadcn badge, checkbox, sonner toaster, and useNow hook

- badge, checkbox, sonner shadcn primitives
- useNow hook for client-side now() with re-render
- mount <Toaster /> in the frontend root layout
This commit is contained in:
Jason Fraley 2026-08-10 16:01:43 -04:00
parent fc27f11ab8
commit 0b6e00b99c
5 changed files with 107 additions and 0 deletions

View file

@ -12,6 +12,7 @@ import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQu
import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts";
import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime";
import { LandingPage } from "@/components/frontend/LandingPage";
import { Toaster } from "@/components/ui/sonner";
export const metadata: Metadata = {
description: "A gamified platform for Polaris Task Force, an Arma 3 unit.",
@ -91,6 +92,7 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
)}
{user && <GameTickRealtime />}
{isLogistics && <ShipmentToasts />}
<Toaster />
</body>
</html>
);

View file

@ -0,0 +1,48 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
outline:
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
link: "text-primary underline-offset-4 [a&]:hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View file

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import { CheckIcon } from "lucide-react"
import { Checkbox as CheckboxPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View file

@ -0,0 +1,11 @@
"use client";
import { Toaster as Sonner } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
return <Sonner theme="dark" className="toaster group" {...props} />;
};
export { Toaster };

14
src/hooks/useNow.ts Normal file
View file

@ -0,0 +1,14 @@
"use client";
import { useEffect, useState } from "react";
export function useNow(intervalMs = 1000): number {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
const interval = setInterval(() => setNow(Date.now()), intervalMs);
return () => clearInterval(interval);
}, [intervalMs]);
return now;
}