From 37cec4bb8ea9c9ff2cbd082c9c07fc2cb185a160 Mon Sep 17 00:00:00 2001 From: RAZ'D Date: Mon, 9 Feb 2026 05:04:00 +0000 Subject: [PATCH] Style auth UI templates with Tailwind --- README.md | 52 ++++++++++++++++++- src/components/AuthGate.tsx | 40 +++++++++++++++ src/components/LoginForm.tsx | 90 +++++++++++++++++++++++++++++++++ src/components/LogoutButton.tsx | 40 +++++++++++++++ src/index.ts | 21 ++++---- 5 files changed, 233 insertions(+), 10 deletions(-) create mode 100644 src/components/AuthGate.tsx create mode 100644 src/components/LoginForm.tsx create mode 100644 src/components/LogoutButton.tsx diff --git a/README.md b/README.md index 50f585b..0c1739f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ I got tired of copy-pasting this code around, so I made a library. Maybe you'll - Manages login/logout state - Stores tokens in localStorage (or wherever you want) - Gives you hooks to check if someone's logged in +- Provides optional UI templates (LoginForm, LogoutButton, AuthGate) - Protects routes that need authentication - Handles token refresh when you tell it to @@ -27,7 +28,6 @@ I got tired of copy-pasting this code around, so I made a library. Maybe you'll - No OAuth flows (build that yourself) - No cookie auth (we use tokens) - No automatic background token refresh (you call refresh when needed) -- No fancy UI components - No backend (you need to build your own API) ## Install @@ -62,6 +62,31 @@ function App() { That's it. Now you can use the hooks anywhere. Pretty simple right? +## Prebuilt UI templates + +If you want the boilerplate UI handled for you, the library ships with simple, optional UI components: + +```jsx +import { AuthGate, LoginForm, LogoutButton } from 'authbase-react'; + +function App() { + return ( + + + + + + + ); +} +``` + +- `AuthGate` protects everything inside it and shows a login form by default when unauthenticated. +- `LoginForm` is a minimal sign-in form wired to `signIn`. +- `LogoutButton` signs the user out with one click. + +These templates are styled with Tailwind (shadcn-ui inspired), so your app should already have Tailwind configured to see the intended design and animations. + ## Make a login form ```jsx @@ -201,6 +226,9 @@ Response: { user: object } ``` +Failure cases: +- 401/403 for invalid credentials (will surface as an error in the auth state) +- 4xx/5xx for API issues (will surface as an error in the auth state) **Refresh endpoint (optional):** ``` @@ -210,6 +238,9 @@ Response: { access_token: string } ``` +Failure cases: +- 401/403 for expired/invalid refresh token (will transition to unauthenticated) +- 4xx/5xx for API issues (will surface as an error in the auth state) **Logout endpoint (optional):** ``` @@ -217,6 +248,8 @@ POST /auth/logout Headers: Authorization: Bearer {access_token} Response: 204 ``` +Failure cases: +- 4xx/5xx errors are captured but local auth state still clears That's all we support right now. If your API looks different, this library won't work for you (yet). Sorry bout that. @@ -251,6 +284,23 @@ const { **useIsAuthenticated()** - Just a boolean, yep +## State machine overview + +Authbase-react is intentionally deterministic. These are the only possible states and transitions: + +**States** +- `idle` → initial state before storage is checked +- `loading` → a login/logout/refresh/init is in progress +- `authenticated` → user + access token are present +- `unauthenticated` → no valid session +- `error` → an operation failed (error is stored in state) + +**Common transitions** +- `idle` → `loading` → `authenticated | unauthenticated` +- `unauthenticated` → `loading` → `authenticated` (login success) +- `authenticated` → `loading` → `unauthenticated` (logout) +- `authenticated` → `loading` → `authenticated | unauthenticated` (refresh) + ## Philosophy This library is intentionally boring. There's no clever tricks, no abstractions, no "magic". It's just a state machine that stores some data. Boring is good sometimes. diff --git a/src/components/AuthGate.tsx b/src/components/AuthGate.tsx new file mode 100644 index 0000000..90187f7 --- /dev/null +++ b/src/components/AuthGate.tsx @@ -0,0 +1,40 @@ +// Auth gate - protects the entire tree and renders a default login form + +import React from 'react'; +import { useAuth } from '../hooks/useAuth'; +import { LoginForm } from './LoginForm'; + +interface AuthGateProps { + children: React.ReactNode; + loginFallback?: React.ReactNode; + loadingFallback?: React.ReactNode; + errorFallback?: React.ReactNode; +} + +export function AuthGate({ + children, + loginFallback, + loadingFallback = null, + errorFallback, +}: AuthGateProps) { + const { isAuthenticated, isLoading, error } = useAuth(); + const defaultLogin = ( +
+ +
+ ); + + if (isLoading) { + return <>{loadingFallback}; + } + + if (error) { + return <>{errorFallback ?? loginFallback ?? defaultLogin}; + } + + if (!isAuthenticated) { + return <>{loginFallback ?? defaultLogin}; + } + + return <>{children}; +} diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx new file mode 100644 index 0000000..27e6892 --- /dev/null +++ b/src/components/LoginForm.tsx @@ -0,0 +1,90 @@ +// Prebuilt login form - keeps auth UI boilerplate small + +import React, { useState } from 'react'; +import { useAuth } from '../hooks/useAuth'; + +interface LoginFormProps { + identifierLabel?: string; + secretLabel?: string; + submitLabel?: string; + className?: string; + onSuccess?: () => void; + onError?: (error: Error) => void; +} + +export function LoginForm({ + identifierLabel = 'Email', + secretLabel = 'Password', + submitLabel = 'Sign in', + className, + onSuccess, + onError, +}: LoginFormProps) { + const { signIn, isLoading, error } = useAuth(); + const [identifier, setIdentifier] = useState(''); + const [secret, setSecret] = useState(''); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + try { + await signIn(identifier, secret); + onSuccess?.(); + } catch (caught) { + onError?.(caught as Error); + } + }; + + return ( +
+
+ + setIdentifier(event.target.value)} + autoComplete="username" + /> +
+
+ + setSecret(event.target.value)} + autoComplete="current-password" + /> +
+ + {error && ( +

+ {error.message} +

+ )} +
+ ); +} diff --git a/src/components/LogoutButton.tsx b/src/components/LogoutButton.tsx new file mode 100644 index 0000000..9b7119e --- /dev/null +++ b/src/components/LogoutButton.tsx @@ -0,0 +1,40 @@ +// Prebuilt logout button - quick sign out UI + +import React from 'react'; +import { useAuth } from '../hooks/useAuth'; + +interface LogoutButtonProps { + label?: string; + className?: string; + onSignedOut?: () => void; +} + +export function LogoutButton({ label = 'Sign out', className, onSignedOut }: LogoutButtonProps) { + const { signOut, isLoading } = useAuth(); + + const handleClick = async () => { + await signOut(); + onSignedOut?.(); + }; + + return ( + + ); +} diff --git a/src/index.ts b/src/index.ts index 5491d93..d3192ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,12 @@ -// Public API - only export what consumers need, keep it simple - -export { AuthProvider } from './AuthProvider'; -export { RequireAuth } from './components/RequireAuth'; -export { useAuth } from './hooks/useAuth'; -export { useUser } from './hooks/useUser'; -export { useIsAuthenticated } from './hooks/useIsAuthenticated'; - -export type { AuthConfig, User } from './types'; \ No newline at end of file +// Public API - only export what consumers need, keep it simple + +export { AuthProvider } from './AuthProvider'; +export { RequireAuth } from './components/RequireAuth'; +export { AuthGate } from './components/AuthGate'; +export { LoginForm } from './components/LoginForm'; +export { LogoutButton } from './components/LogoutButton'; +export { useAuth } from './hooks/useAuth'; +export { useUser } from './hooks/useUser'; +export { useIsAuthenticated } from './hooks/useIsAuthenticated'; + +export type { AuthConfig, User } from './types';