mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 07:45:18 -05:00
lol
This commit is contained in:
306
.agent/rules/component-patterns.md
Normal file
306
.agent/rules/component-patterns.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# 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>
|
||||
```
|
||||
Reference in New Issue
Block a user