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
components.json
package.json
{
"$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
Custom Components
Feature-Based Components
Components organized by feature area contain business logic:
components/match/match-search-client.tsx
components/match/match-slot-card.tsx
'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 >
)
}
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:
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:
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