From 7a42c08927cae9dee93641bd58e932beab6d66c5 Mon Sep 17 00:00:00 2001 From: Amr Gaber Date: Mon, 30 Mar 2026 18:57:48 -0500 Subject: [PATCH] feat: apply production learnings from pbx and vagrant-story Add Button loading state with width preservation, extra button sizes (xs, icon-xs, icon-sm, icon-lg), auth re-validation on tab focus via visibilitychange, and name field to auth context. --- src/components/ui/button.tsx | 33 ++++++++++++++++++++++++++++++++- src/lib/auth.tsx | 21 +++++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 8761b84..4340f17 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,6 +1,7 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" +import { LoaderCircle } from "lucide-react" import { cn } from "@/lib/utils" @@ -22,9 +23,13 @@ const buttonVariants = cva( }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", }, }, defaultVariants: { @@ -39,19 +44,45 @@ function Button({ variant, size, asChild = false, + loading = false, + disabled, + children, ...props }: React.ComponentProps<"button"> & VariantProps & { asChild?: boolean + loading?: boolean }) { const Comp = asChild ? Slot : "button" + const ref = React.useRef(null) + const [savedWidth, setSavedWidth] = React.useState( + undefined + ) + + React.useLayoutEffect(() => { + if (ref.current && !loading) { + setSavedWidth(ref.current.offsetWidth) + } + }, [loading]) return ( + > + {asChild ? ( + children + ) : ( + <> + {loading && } + {children} + + )} + ) } diff --git a/src/lib/auth.tsx b/src/lib/auth.tsx index 4fa9583..073100c 100644 --- a/src/lib/auth.tsx +++ b/src/lib/auth.tsx @@ -16,6 +16,7 @@ interface AuthContextValue { isLoading: boolean email: string | null userId: string | null + name: string | null login: (email: string) => void logout: () => Promise checkAuth: () => Promise @@ -31,12 +32,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { localStorage.getItem(EMAIL_KEY) ) const [userId, setUserId] = useState(null) + const [name, setName] = useState(null) const clearState = useCallback(() => { localStorage.removeItem(EMAIL_KEY) setIsAuthenticated(false) setEmail(null) setUserId(null) + setName(null) queryClient.clear() }, [queryClient]) @@ -59,10 +62,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { try { const user = await api .get("auth/me") - .json<{ id: string; email: string }>() + .json<{ id: string; email: string; name: string | null }>() setIsAuthenticated(true) setEmail(user.email) setUserId(user.id) + setName(user.name) localStorage.setItem(EMAIL_KEY, user.email) } catch { clearState() @@ -75,6 +79,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { checkAuth() }, [checkAuth]) + // Re-validate auth when the user returns to the tab after being away + useEffect(() => { + function handleVisibilityChange() { + if (document.visibilityState === "visible") { + checkAuth() + } + } + document.addEventListener("visibilitychange", handleVisibilityChange) + return () => + document.removeEventListener("visibilitychange", handleVisibilityChange) + }, [checkAuth]) + useEffect(() => { setOnUnauthorized(() => { clearState() @@ -87,11 +103,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { isLoading, email, userId, + name, login, logout, checkAuth, }), - [isAuthenticated, isLoading, email, userId, login, logout, checkAuth] + [isAuthenticated, isLoading, email, userId, name, login, logout, checkAuth] ) return {children}