mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 09:45:17 -05:00
307 lines
6.9 KiB
Markdown
307 lines
6.9 KiB
Markdown
# 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>
|
|
```
|