Skip to content

Component Library

Overview

Jubiloop uses a custom component library built on top of shadcn/ui components. The library provides accessible, customizable, and reusable UI components that follow the project's design system.

Architecture

Component Organization

packages/ui/                 # Shared UI package
├── src/
│   ├── components/         # UI components
│   │   ├── button.tsx
│   │   ├── card.tsx
│   │   ├── form.tsx
│   │   └── ...
│   └── lib/
│       └── utils.ts        # Utility functions

apps/webapp/
├── src/
│   └── components/         # App-specific components
│       ├── auth/          # Authentication components
│       └── Header.tsx     # Navigation header

Component Layers

  1. Base Components (packages/ui): Reusable UI primitives
  2. App Components (apps/webapp/components): Business logic components
  3. Page Components (apps/webapp/routes): Route-specific components

Core Components

Button

Versatile button component with multiple variants and sizes:

typescript
import { Button } from '@jubiloop/ui/components/button'

<Button variant="default" size="md">
  Click me
</Button>

<Button variant="outline" size="sm">
  Cancel
</Button>

<Button variant="destructive" disabled>
  Delete
</Button>

Variants:

  • default - Primary action button
  • outline - Secondary action
  • ghost - Minimal styling
  • destructive - Dangerous actions
  • link - Styled as a link

Form Components

Built with React Hook Form and Radix UI:

typescript
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@jubiloop/ui/components/form'
import { Input } from '@jubiloop/ui/components/input'

<Form {...form}>
  <FormField
    control={form.control}
    name="email"
    render={({ field }) => (
      <FormItem>
        <FormLabel>Email</FormLabel>
        <FormControl>
          <Input placeholder="Enter email" {...field} />
        </FormControl>
        <FormMessage />
      </FormItem>
    )}
  />
</Form>

Card

Container component for content sections:

typescript
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@jubiloop/ui/components/card'

<Card>
  <CardHeader>
    <CardTitle>Event Details</CardTitle>
    <CardDescription>View and manage your event</CardDescription>
  </CardHeader>
  <CardContent>
    {/* Content */}
  </CardContent>
  <CardFooter>
    <Button>Save Changes</Button>
  </CardFooter>
</Card>

Brand logo component with responsive sizing:

typescript
import { Logo } from '@jubiloop/ui/components/logo'

<Logo className="h-8 w-auto" />

Toast Notifications

Using Sonner for toast notifications:

typescript
import { toast } from 'sonner'

// Success toast
toast.success('Event created successfully!')

// Error toast
toast.error('Failed to save changes')

// Promise toast
toast.promise(saveData(), {
  loading: 'Saving...',
  success: 'Saved successfully!',
  error: 'Failed to save',
})

Business Components

Authentication Components

SignInForm

Complete sign-in form with validation:

typescript
import SignInForm from '@/components/auth/SignInForm'

<SignInForm
  onSuccess={() => navigate('/dashboard')}
  redirectTo="/dashboard"
/>

Features:

  • Email/password validation
  • Remember me option
  • Error handling
  • Loading states
  • CSRF protection

SignUpForm

User registration form:

typescript
import SignUpForm from '@/components/auth/SignUpForm'

<SignUpForm
  onSuccess={() => navigate('/dashboard')}
/>

Features:

  • Multi-field validation
  • Password strength requirements
  • Terms acceptance
  • Duplicate email detection

Header Component

Main navigation header with responsive design:

typescript
import Header from '@/components/Header'

<Header />

Features:

  • User menu with avatar
  • Logout functionality
  • Responsive mobile menu
  • Active route highlighting

Component Patterns

Composition Pattern

Build complex UIs by composing smaller components:

typescript
function EventCard({ event }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{event.name}</CardTitle>
        <CardDescription>{event.date}</CardDescription>
      </CardHeader>
      <CardContent>
        <div className="space-y-2">
          <Label>Location</Label>
          <p>{event.location}</p>
        </div>
      </CardContent>
      <CardFooter className="flex justify-between">
        <Button variant="outline">Edit</Button>
        <Button>View Details</Button>
      </CardFooter>
    </Card>
  )
}

Compound Components

Related components that work together:

typescript
<Form.Root>
  <Form.Field name="email">
    <Form.Label>Email</Form.Label>
    <Form.Control>
      <Input type="email" />
    </Form.Control>
    <Form.Error />
  </Form.Field>
</Form.Root>

Controlled Components

Form inputs with controlled state:

typescript
function ControlledInput() {
  const [value, setValue] = useState('')

  return (
    <Input
      value={value}
      onChange={(e) => setValue(e.target.value)}
      placeholder="Controlled input"
    />
  )
}

Styling System

Tailwind CSS Classes

Components use Tailwind CSS for styling:

typescript
<div className="flex items-center justify-between p-4 bg-white rounded-lg shadow">
  <h2 className="text-lg font-semibold text-gray-900">Title</h2>
  <Button size="sm">Action</Button>
</div>

CSS Variables

Theme customization through CSS variables:

css
:root {
  --background: 0 0% 100%;
  --foreground: 240 10% 3.9%;
  --primary: 240 5.9% 10%;
  --primary-foreground: 0 0% 98%;
  /* ... more variables */
}

Utility Functions

Helper for conditional classes:

typescript
import { cn } from '@jubiloop/ui/lib/utils'

<div className={cn(
  "base-classes",
  isActive && "active-classes",
  isDisabled && "disabled-classes"
)} />

Accessibility

ARIA Attributes

Components include proper ARIA attributes:

typescript
<Button
  aria-label="Close dialog"
  aria-pressed={isPressed}
  aria-disabled={isDisabled}
>
  Close
</Button>

Keyboard Navigation

All interactive components support keyboard navigation:

  • Tab navigation
  • Enter/Space activation
  • Escape to close
  • Arrow keys for menus

Screen Reader Support

Components include screen reader announcements:

typescript
<FormMessage role="alert" aria-live="polite">
  {error.message}
</FormMessage>

Component Guidelines

Creating New Components

  1. Start with shadcn/ui: Check if a base component exists
  2. Follow Naming Conventions: PascalCase for components
  3. Include TypeScript Types: Define prop interfaces
  4. Add JSDoc Comments: Document component usage
  5. Consider Accessibility: Include ARIA attributes
  6. Write Tests: Unit tests for logic, integration for behavior

Component Template

typescript
import { forwardRef } from 'react'
import { cn } from '@jubiloop/ui/lib/utils'

export interface ComponentNameProps
  extends React.HTMLAttributes<HTMLDivElement> {
  /** Component variant */
  variant?: 'default' | 'secondary'
  /** Component size */
  size?: 'sm' | 'md' | 'lg'
}

/**
 * ComponentName - Brief description
 *
 * @example
 * <ComponentName variant="secondary" size="lg">
 *   Content
 * </ComponentName>
 */
const ComponentName = forwardRef<HTMLDivElement, ComponentNameProps>(
  ({ className, variant = 'default', size = 'md', ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(
          'base-styles',
          variants[variant],
          sizes[size],
          className
        )}
        {...props}
      />
    )
  }
)

ComponentName.displayName = 'ComponentName'

export { ComponentName }

Testing Components

Unit Tests

typescript
import { render, screen } from '@testing-library/react'
import { Button } from '@jubiloop/ui/components/button'

describe('Button', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('applies variant classes', () => {
    render(<Button variant="outline">Test</Button>)
    expect(screen.getByRole('button')).toHaveClass('border')
  })
})

Integration Tests

typescript
import { render, screen, fireEvent } from '@testing-library/react'
import SignInForm from '@/components/auth/SignInForm'

test('submits form with valid data', async () => {
  const onSubmit = vi.fn()
  render(<SignInForm onSubmit={onSubmit} />)

  fireEvent.change(screen.getByLabelText('Email'), {
    target: { value: 'test@example.com' }
  })

  fireEvent.change(screen.getByLabelText('Password'), {
    target: { value: 'password123' }
  })

  fireEvent.click(screen.getByText('Sign In'))

  await waitFor(() => {
    expect(onSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123'
    })
  })
})

Component Documentation

Storybook (Future)

Component documentation and visual testing:

typescript
import type { Meta, StoryObj } from '@storybook/react'

import { Button } from '@jubiloop/ui/components/button'

const meta: Meta<typeof Button> = {
  title: 'UI/Button',
  component: Button,
  argTypes: {
    variant: {
      control: 'select',
      options: ['default', 'outline', 'ghost', 'destructive', 'link'],
    },
  },
}

export default meta

export const Default: StoryObj<typeof Button> = {
  args: {
    children: 'Button',
    variant: 'default',
  },
}

Performance Optimization

Code Splitting

Lazy load heavy components:

typescript
const HeavyComponent = lazy(() => import('./HeavyComponent'))

<Suspense fallback={<Spinner />}>
  <HeavyComponent />
</Suspense>

Memoization

Prevent unnecessary re-renders:

typescript
const MemoizedComponent = memo(({ data }) => {
  return <ExpensiveComponent data={data} />
}, (prevProps, nextProps) => {
  return prevProps.data.id === nextProps.data.id
})

Virtual Scrolling

For long lists:

typescript
import { VirtualList } from '@tanstack/react-virtual'

<VirtualList
  height={600}
  itemCount={items.length}
  itemSize={50}
  renderItem={({ index }) => (
    <ListItem item={items[index]} />
  )}
/>

Built with ❤️ by the Jubiloop team