Skip to main content

Shared Components

Both the Backend Dashboard and Heimdall ID share a common set of UI components built with React and TailwindCSS.

UI Components

Button

A versatile button component with multiple variants and sizes.

import { Button } from "@/components/ui/Button";

// Variants
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>

// Sizes
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>

// States
<Button disabled>Disabled</Button>
<Button loading>Loading...</Button>

// With icons
<Button>
<IconPlus className="mr-2" />
Add Item
</Button>

Props:

PropTypeDefaultDescription
variant'primary' | 'secondary' | 'outline' | 'ghost' | 'danger''primary'Button style
size'sm' | 'md' | 'lg''md'Button size
disabledbooleanfalseDisabled state
loadingbooleanfalseLoading state
type'button' | 'submit' | 'reset''button'HTML button type
onClick() => void-Click handler

Alert

Display alert messages with different severity levels.

import { Alert } from "@/components/ui/Alert";

<Alert type="success" title="Success!">
Your changes have been saved successfully.
</Alert>

<Alert type="error" title="Error">
Something went wrong. Please try again.
</Alert>

<Alert type="warning" title="Warning">
This action cannot be undone.
</Alert>

<Alert type="info" title="Info">
Your session will expire in 5 minutes.
</Alert>

// Dismissible
<Alert type="info" dismissible onDismiss={() => setShow(false)}>
Click the X to dismiss this alert.
</Alert>

Props:

PropTypeDefaultDescription
type'success' | 'error' | 'warning' | 'info''info'Alert type
titlestring-Optional title
childrenReactNode-Alert content
dismissiblebooleanfalseShow dismiss button
onDismiss() => void-Dismiss callback

A modal dialog component for confirmations and forms.

import { Modal } from "@/components/ui/Modal";

function ConfirmDialog() {
const [isOpen, setIsOpen] = useState(false);

return (
<>
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>

<Modal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Confirm Action"
size="md"
>
<p>Are you sure you want to proceed with this action?</p>

<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button variant="danger" onClick={handleConfirm}>
Confirm
</Button>
</div>
</Modal>
</>
);
}

Props:

PropTypeDefaultDescription
isOpenboolean-Whether modal is open
onClose() => void-Close callback
titlestring-Modal title
size'sm' | 'md' | 'lg' | 'xl''md'Modal size
childrenReactNode-Modal content
closeOnOverlayClickbooleantrueClose on backdrop click
closeOnEscapebooleantrueClose on Escape key

FloatingInput

An input field with a floating label animation.

import { FloatingInput } from "@/components/ui/FloatingInput";

<FloatingInput
label="Email Address"
type="email"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={errors.email}
required
/>

<FloatingInput
label="Password"
type="password"
name="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={errors.password}
required
/>

// With helper text
<FloatingInput
label="Username"
helperText="This will be your public display name"
/>

Props:

PropTypeDefaultDescription
labelstring-Input label
typestring'text'Input type
namestring-Input name
valuestring-Input value
onChange(e) => void-Change handler
errorstring-Error message
helperTextstring-Helper text
requiredbooleanfalseRequired field
disabledbooleanfalseDisabled state

LoadingSpinner

A loading spinner component.

import { LoadingSpinner } from "@/components/ui/LoadingSpinner";

// Sizes
<LoadingSpinner size="sm" />
<LoadingSpinner size="md" />
<LoadingSpinner size="lg" />

// With text
<div className="flex items-center gap-2">
<LoadingSpinner size="sm" />
<span>Loading...</span>
</div>

// Full page
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner size="lg" />
</div>

Props:

PropTypeDefaultDescription
size'sm' | 'md' | 'lg''md'Spinner size
classNamestring-Additional classes

The Elcto brand logo component.

import { ElctoLogo } from "@/components/ui/ElctoLogo";

// Variants
<ElctoLogo variant="full" /> // Full logo with text
<ElctoLogo variant="icon" /> // Icon only
<ElctoLogo variant="wordmark" /> // Text only

// Sizes
<ElctoLogo size="sm" />
<ElctoLogo size="md" />
<ElctoLogo size="lg" />

// Dark/Light mode
<ElctoLogo theme="dark" />
<ElctoLogo theme="light" />

Auth Components

ProtectedRoute

Wrap pages that require authentication.

import { ProtectedRoute } from "@/components/auth/ProtectedRoute";

export default function DashboardPage() {
return (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
);
}

// With required permissions
<ProtectedRoute requiredPermissions={["gps:write", "users:read"]}>
<AdminPanel />
</ProtectedRoute>

// With custom fallback
<ProtectedRoute fallback={<CustomLoginPrompt />}>
<Content />
</ProtectedRoute>

Props:

PropTypeDefaultDescription
childrenReactNode-Protected content
requiredPermissionsstring[]-Required permissions
fallbackReactNode<LoginRedirect />Fallback component

ProtectedContent

Conditionally render content based on auth state.

import { ProtectedContent } from "@/components/auth/ProtectedContent";

<ProtectedContent>
{/* Only shown when authenticated */}
<UserMenu />
</ProtectedContent>

<ProtectedContent fallback={<LoginButton />}>
<LogoutButton />
</ProtectedContent>

<ProtectedContent requiredPermissions={["admin"]}>
<AdminTools />
</ProtectedContent>

Props:

PropTypeDefaultDescription
childrenReactNode-Protected content
fallbackReactNodenullFallback when not authenticated
requiredPermissionsstring[]-Required permissions

UserButton

Display user info with dropdown menu.

import { UserButton } from "@/components/auth/UserButton";

// Basic usage
<UserButton />

// With custom menu items
<UserButton
menuItems={[
{ label: "Settings", href: "/settings" },
{ label: "Help", href: "/help" },
]}
/>

Props:

PropTypeDefaultDescription
menuItemsMenuItem[]-Additional menu items
showEmailbooleantrueShow email in dropdown

Layout Components

DashboardLayout

The main layout for authenticated dashboard pages.

import { DashboardLayout } from "@/components/layouts/DashboardLayout";

export default function Page() {
return (
<DashboardLayout
title="GPS Data"
breadcrumbs={[
{ label: "Dashboard", href: "/dashboard" },
{ label: "GPS Data" }
]}
>
<GpsDataContent />
</DashboardLayout>
);
}

Props:

PropTypeDefaultDescription
childrenReactNode-Page content
titlestring-Page title
breadcrumbsBreadcrumb[]-Breadcrumb navigation

OverlayLayout

Full-screen overlay layout for modals and overlays.

import { OverlayLayout } from "@/layouts/OverlayLayout";

export default function ModalPage() {
return (
<OverlayLayout onClose={() => router.back()}>
<div className="bg-white rounded-lg p-6 max-w-md w-full">
<h2>Modal Content</h2>
{/* ... */}
</div>
</OverlayLayout>
);
}

The site footer component.

import { Footer } from "@/components/layout/Footer";

export default function Layout({ children }) {
return (
<>
<main>{children}</main>
<Footer />
</>
);
}

Providers

SessionProvider

Wrap your app with the session provider for auth state.

// src/app/layout.tsx
import { SessionProvider } from "@/components/providers/SessionProvider";

export default function RootLayout({ children }) {
return (
<html>
<body>
<SessionProvider>
{children}
</SessionProvider>
</body>
</html>
);
}

ApolloWrapper

GraphQL client provider (Backend Dashboard only).

// src/app/layout.tsx
import { ApolloWrapper } from "@/components/providers/ApolloWrapper";

export default function RootLayout({ children }) {
return (
<html>
<body>
<SessionProvider>
<ApolloWrapper>
{children}
</ApolloWrapper>
</SessionProvider>
</body>
</html>
);
}

Styling

All components use TailwindCSS for styling. Common patterns:

Color Tokens

/* Primary colors */
bg-primary-500
text-primary-600
border-primary-400

/* Semantic colors */
bg-success-100 text-success-700
bg-error-100 text-error-700
bg-warning-100 text-warning-700
bg-info-100 text-info-700

Spacing

/* Padding */
p-4 /* 1rem */
px-6 py-3 /* horizontal 1.5rem, vertical 0.75rem */

/* Margin */
m-4
mt-2 mb-4
mx-auto /* center horizontally */

Responsive Design

/* Mobile first */
w-full md:w-1/2 lg:w-1/3

/* Hide/show */
hidden md:block
block md:hidden

Best Practices

Component Composition

// Good: Compose smaller components
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>
Content here
</CardContent>
</Card>

// Avoid: Monolithic components with many props
<Card
title="Title"
headerActions={...}
content={...}
footerActions={...}
/>

Error Boundaries

import { ErrorBoundary } from "@/components/ErrorBoundary";

<ErrorBoundary fallback={<ErrorMessage />}>
<RiskyComponent />
</ErrorBoundary>

Loading States

function DataTable() {
const { data, isLoading, error } = useQuery(...);

if (isLoading) return <LoadingSpinner />;
if (error) return <Alert type="error">{error.message}</Alert>;

return <Table data={data} />;
}

Next Steps