Files
thrillwiki_django_no_react/.agent/rules/component-patterns.md
pacnpal 1adba1b804 lol
2026-01-02 07:58:58 -05:00

6.9 KiB

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

<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

<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

<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
<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.

<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.

<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

<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:

<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)
<EmptyState
  icon="Search"
  title="No results found"
  description="Try adjusting your search or filters"
>
  <Button @click="clearFilters">Clear Filters</Button>
</EmptyState>