# ThrillWiki Component Patterns

Guidelines for building UI components consistent with ThrillWiki's design system.

## Component Hierarchy

```
components/
├── layout/           # Page structure
│   ├── Header.vue
│   ├── Footer.vue
│   ├── Sidebar.vue
│   └── PageContainer.vue
├── ui/               # Base components (shadcn/ui style)
│   ├── Button.vue
│   ├── Card.vue
│   ├── Input.vue
│   ├── Badge.vue
│   ├── Avatar.vue
│   ├── Modal.vue
│   └── ...
├── entity/           # Domain-specific cards
│   ├── ParkCard.vue
│   ├── RideCard.vue
│   ├── ReviewCard.vue
│   ├── CreditCard.vue
│   └── CompanyCard.vue
├── forms/            # Form components
│   ├── ParkForm.vue
│   ├── ReviewForm.vue
│   └── ...
└── specialty/        # Complex/unique components
    ├── SearchAutocomplete.vue
    ├── Map.vue
    ├── ImageGallery.vue
    ├── UnitDisplay.vue
    └── RatingDisplay.vue
```

## Base Components (ui/)

### Card
```vue
<template>
  <div :class="[
    'rounded-lg border bg-card text-card-foreground',
    interactive && 'hover:shadow-md transition-shadow cursor-pointer',
    className
  ]">
    <slot />
  </div>
</template>

<script setup lang="ts">
defineProps<{
  interactive?: boolean
  className?: string
}>()
</script>
```

### Button
```vue
<template>
  <button :class="[buttonVariants({ variant, size }), className]">
    <slot />
  </button>
</template>

<script setup lang="ts">
const props = defineProps<{
  variant?: 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive'
  size?: 'default' | 'sm' | 'lg' | 'icon'
  className?: string
}>()
</script>
```

### Badge
```vue
<template>
  <span :class="[badgeVariants({ variant }), className]">
    <slot />
  </span>
</template>

<script setup lang="ts">
defineProps<{
  variant?: 'default' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline'
  className?: string
}>()
</script>
```

## Entity Cards

### ParkCard
Displays park preview with image, name, location, stats, and status.

**Required Props:**
- `park: Park` - Park object

**Displays:**
- Park image (with fallback)
- Park name
- Location (city, country)
- Ride count
- Average rating
- Status badge (Operating/Closed/Under Construction)

**Interactions:**
- Click navigates to park detail page
- Hover shows elevation shadow

```vue
<template>
  <NuxtLink :to="`/parks/${park.slug}`">
    <Card interactive>
      <div class="aspect-video relative overflow-hidden rounded-t-lg">
        <NuxtImg
          :src="park.image || '/placeholder-park.jpg'"
          :alt="park.name"
          class="object-cover w-full h-full"
        />
      </div>
      <div class="p-4">
        <h3 class="font-semibold text-lg line-clamp-1">{{ park.name }}</h3>
        <p class="text-sm text-muted-foreground flex items-center gap-1">
          <MapPin class="w-4 h-4" />
          {{ park.city }}, {{ park.country }}
        </p>
        <div class="flex items-center justify-between mt-2">
          <span class="text-sm">🎢 {{ park.rideCount }} rides</span>
          <RatingDisplay :rating="park.averageRating" size="sm" />
        </div>
        <Badge :variant="statusVariant" class="mt-2">
          {{ park.status }}
        </Badge>
      </div>
    </Card>
  </NuxtLink>
</template>
```

### RideCard
Similar structure to ParkCard, but shows:
- Ride image
- Ride name
- Park name (linked)
- Key specs (speed, height)
- Type badge + Status badge

### ReviewCard
Displays user review with:
- User avatar + username
- Rating (5-star display)
- Review date (relative)
- Review text
- Helpful votes (👍 count)
- Actions (Reply, Report)

### CreditCard
For user's ride credit list:
- Ride thumbnail
- Ride name + park name
- Ride count with +/- controls
- Last ridden date
- Edit button

## Specialty Components

### SearchAutocomplete
Global search with instant results.

**Features:**
- Debounced input (300ms)
- Results grouped by type (Parks, Rides, Companies)
- Keyboard navigation
- Click or Enter to navigate
- Empty state handling

**Implementation Notes:**
- Use `useFetch` with `watch` for reactive searching
- Show loading skeleton while fetching
- Limit results to 10 per category
- Highlight matching text

### UnitDisplay
Converts and displays values in user's preferred units.

```vue
<template>
  <span>{{ formattedValue }}</span>
</template>

<script setup lang="ts">
const props = defineProps<{
  value: number
  type: 'speed' | 'height' | 'length' | 'weight'
  showBoth?: boolean // Show both metric and imperial
}>()

const { preferredUnits } = useUnits()

const formattedValue = computed(() => {
  // Convert and format based on type and preference
})
</script>
```

### RatingDisplay
Star rating visualization.

```vue
<template>
  <div class="flex items-center gap-1">
    <div class="flex">
      <Star
        v-for="i in 5"
        :key="i"
        :class="[
          'w-4 h-4',
          i <= Math.round(rating) ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'
        ]"
      />
    </div>
    <span class="text-sm font-medium">{{ rating.toFixed(1) }}</span>
    <span v-if="count" class="text-sm text-muted-foreground">({{ count }})</span>
  </div>
</template>
```

### Map (Leaflet)
Interactive map for parks nearby feature.

**Features:**
- Marker clusters for dense areas
- Custom markers for different park types
- Popup on marker click with park preview
- Zoom controls
- Full-screen toggle
- User location marker (if permitted)

## Form Components

### Standard Form Structure
```vue
<template>
  <form @submit.prevent="handleSubmit">
    <div class="space-y-4">
      <FormField label="Name" :error="errors.name">
        <Input v-model="form.name" />
      </FormField>
      
      <FormField label="Description">
        <Textarea v-model="form.description" />
      </FormField>
      
      <div class="flex gap-2 justify-end">
        <Button variant="outline" @click="$emit('cancel')">Cancel</Button>
        <Button type="submit" :loading="isSubmitting">Save</Button>
      </div>
    </div>
  </form>
</template>
```

### Validation
- Use Zod for schema validation
- Display errors inline below fields
- Disable submit button while invalid
- Show loading state during submission

## Loading States

### Skeleton Loading
All cards should have skeleton states:
```vue
<template>
  <Card v-if="loading">
    <Skeleton class="aspect-video rounded-t-lg" />
    <div class="p-4 space-y-2">
      <Skeleton class="h-5 w-3/4" />
      <Skeleton class="h-4 w-1/2" />
      <Skeleton class="h-4 w-1/4" />
    </div>
  </Card>
  <ActualCard v-else :data="data" />
</template>
```

### Empty States
Provide clear empty states with:
- Relevant icon
- Clear message
- Suggested action (button or link)

```vue
<EmptyState
  icon="Search"
  title="No results found"
  description="Try adjusting your search or filters"
>
  <Button @click="clearFilters">Clear Filters</Button>
</EmptyState>
```
