Skip to main content

Overview

The Hub frontend uses shadcn/ui built on top of Radix UI primitives for accessible, customizable components. Components are organized by feature area with a clear separation between UI primitives and business logic.

shadcn/ui Configuration

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,              // React Server Components enabled
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/app/globals.css",
    "baseColor": "slate",
    "cssVariables": true,
    "prefix": ""
  },
  "iconLibrary": "lucide",
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui"
  }
}

Component Directory Structure

components/
├── ui/                    # shadcn/ui components (50+ components)
│   ├── button.tsx
│   ├── card.tsx
│   ├── dialog.tsx
│   ├── dropdown-menu.tsx
│   ├── form.tsx
│   ├── input.tsx
│   ├── select.tsx
│   ├── table.tsx
│   ├── tabs.tsx
│   ├── toast.tsx
│   └── ...
├── admin/                 # Admin-specific components
│   └── admin-sidebar.tsx
├── dashboard/             # Player dashboard components
├── owner/                 # Venue owner components
├── match/                 # Match management components
│   ├── match-search-client.tsx
│   ├── match-create-client.tsx
│   ├── match-detail-client.tsx
│   ├── match-map.tsx
│   └── match-slot-card.tsx
├── venue/                 # Venue-related components
├── forms/                 # Reusable form components
│   └── CityCombobox.tsx
├── auth-form.tsx          # Authentication form
├── theme-provider.tsx     # Theme context provider
└── theme-toggle.tsx       # Dark/light mode toggle

UI Components (shadcn/ui)

Available Components

The application includes 50+ shadcn/ui components:

Forms

Button, Input, Select, Checkbox, Radio, Switch, Textarea, Form, Field

Layout

Card, Separator, Tabs, Accordion, Resizable, Sidebar, Sheet

Feedback

Toast, Alert, Dialog, Drawer, Popover, Tooltip, Progress

Navigation

Dropdown Menu, Navigation Menu, Menubar, Context Menu, Breadcrumb

Data Display

Table, Calendar, Chart, Avatar, Badge, Carousel

Utilities

Scroll Area, Hover Card, Kbd, Skeleton, Spinner, Empty

Component Usage Patterns

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

// Variants
<Button variant="default">Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>

// Sizes
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Icon /></Button>

// With icon
import {Plus} from 'lucide-react'
<Button>
  <Plus className="mr-2 h-4 w-4" />
  Create Match
</Button>

Custom Components

Feature-Based Components

Components organized by feature area contain business logic:
'use client'

import {useState, useEffect} from 'react'
import {Card, CardHeader, CardTitle, CardContent} from '@/components/ui/card'
import {Input} from '@/components/ui/input'
import {Button} from '@/components/ui/button'
import {apiFetchClient} from '@/lib/api'
import type {Match, UserProfile} from '@/types'

interface Props {
  user: UserProfile
}

export function MatchSearchClient({user}: Props) {
  const [matches, setMatches] = useState<Match[]>([])
  const [search, setSearch] = useState('')
  
  useEffect(() => {
    async function loadMatches() {
      const data = await apiFetchClient<Match[]>('/api/matches')
      setMatches(data)
    }
    loadMatches()
  }, [])
  
  return (
    <div className="container mx-auto p-6">
      <Input 
        placeholder="Search matches..."
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />
      <div className="grid gap-4 mt-4">
        {matches.map(match => (
          <MatchSlotCard key={match.id} match={match} />
        ))}
      </div>
    </div>
  )
}

Shared Form Components

Reusable form components with complex logic:
components/forms/CityCombobox.tsx
'use client'

import {useState} from 'react'
import {Check, ChevronsUpDown} from 'lucide-react'
import {cn} from '@/lib/utils'
import {Button} from '@/components/ui/button'
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem} from '@/components/ui/command'
import {Popover, PopoverContent, PopoverTrigger} from '@/components/ui/popover'
import {City} from 'country-state-city'

export function CityCombobox({value, onChange}: {value: string, onChange: (value: string) => void}) {
  const [open, setOpen] = useState(false)
  const cities = City.getCitiesOfCountry('ES')
  
  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button variant="outline" role="combobox" className="w-full justify-between">
          {value || "Select city..."}
          <ChevronsUpDown className="ml-2 h-4 w-4" />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-full p-0">
        <Command>
          <CommandInput placeholder="Search city..." />
          <CommandEmpty>No city found.</CommandEmpty>
          <CommandGroup>
            {cities.map((city) => (
              <CommandItem
                key={city.name}
                onSelect={() => {
                  onChange(city.name)
                  setOpen(false)
                }}
              >
                <Check className={cn("mr-2 h-4 w-4", value === city.name ? "opacity-100" : "opacity-0")} />
                {city.name}
              </CommandItem>
            ))}
          </CommandGroup>
        </Command>
      </PopoverContent>
    </Popover>
  )
}

Styling Utilities

cn() Helper

Combines Tailwind classes with clsx and tailwind-merge:
lib/utils.ts
import {type ClassValue, clsx} from 'clsx'
import {twMerge} from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
Usage:
import {cn} from '@/lib/utils'

<div className={cn(
  'base-class',
  isActive && 'active-class',
  className  // Props className override
)} />

Component Variants (CVA)

Using class-variance-authority for component variants:
components/ui/button.tsx
import {cva, type VariantProps} from 'class-variance-authority'

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md transition-colors',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input hover:bg-accent',
        ghost: 'hover:bg-accent hover:text-accent-foreground'
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 px-3',
        lg: 'h-11 px-8',
        icon: 'h-10 w-10'
      }
    },
    defaultVariants: {
      variant: 'default',
      size: 'default'
    }
  }
)

Icon Library

Lucide React

import {
  Calendar,
  MapPin,
  Users,
  Settings,
  LogOut,
  Plus,
  Search,
  ChevronDown
} from 'lucide-react'

<Button>
  <Plus className="mr-2 h-4 w-4" />
  Create
</Button>
Lucide React provides 1000+ consistent, customizable icons. Import only the icons you need for optimal bundle size.

Theme Support

Theme Provider

components/theme-provider.tsx
'use client'

import {ThemeProvider as NextThemesProvider} from 'next-themes'
import {type ThemeProviderProps} from 'next-themes/dist/types'

export function ThemeProvider({children, ...props}: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

Theme Toggle

components/theme-toggle.tsx
'use client'

import {Moon, Sun} from 'lucide-react'
import {useTheme} from 'next-themes'
import {Button} from '@/components/ui/button'

export function ThemeToggle() {
  const {theme, setTheme} = useTheme()
  
  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
    </Button>
  )
}

Component Best Practices

Server First

Use Server Components by default. Add ‘use client’ only when needed for interactivity

Composition

Build complex UIs by composing simple, reusable components

Type Safety

Define TypeScript interfaces for all component props

Accessibility

Radix UI components are accessible by default - preserve ARIA attributes

Adding New shadcn/ui Components

# Add a new component
npx shadcn@latest add [component-name]

# Examples
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add form
Components are automatically installed to src/components/ui/ with proper configuration.

Next Steps

Frontend Structure

Learn about the Next.js app structure and directory organization

Routing & Auth

Understand routing patterns and authentication integration

Build docs developers (and LLMs) love