mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 16:35:18 -05:00
236 lines
5.3 KiB
Markdown
236 lines
5.3 KiB
Markdown
---
|
|
description: Create a new page in ThrillWiki following project conventions
|
|
---
|
|
|
|
# New Page Workflow
|
|
|
|
Create a new page in ThrillWiki following all conventions and patterns.
|
|
|
|
## Information Gathering
|
|
|
|
Before creating the page, determine:
|
|
|
|
1. **Route**: What URL should this page have?
|
|
2. **Page Type**:
|
|
- List page (shows multiple items)
|
|
- Detail page (shows single item)
|
|
- Form page (create/edit content)
|
|
- Static page (about, contact, etc.)
|
|
3. **Data Requirements**: What data does this page need?
|
|
4. **Authentication**: Public or authenticated only?
|
|
5. **Related Components**: What existing components can be reused?
|
|
|
|
## File Creation Steps
|
|
|
|
### 1. Create the Page Component
|
|
|
|
Location: `frontend/pages/[route].vue` or `frontend/pages/[folder]/[route].vue`
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
// Define page metadata
|
|
definePageMeta({
|
|
// middleware: ['auth'], // If authenticated only
|
|
// layout: 'admin', // If using special layout
|
|
})
|
|
|
|
// Set page head
|
|
useSeoMeta({
|
|
title: 'Page Title | ThrillWiki',
|
|
description: 'Page description for SEO',
|
|
})
|
|
|
|
// Fetch data
|
|
const { data, pending, error } = await useAsyncData('unique-key', () =>
|
|
$fetch('/api/v1/endpoint/')
|
|
)
|
|
|
|
// Handle error
|
|
if (error.value) {
|
|
throw createError({
|
|
statusCode: error.value.statusCode || 500,
|
|
message: error.value.message
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<PageContainer>
|
|
<!-- Breadcrumbs (if applicable) -->
|
|
<Breadcrumbs :items="breadcrumbItems" />
|
|
|
|
<!-- Page Header -->
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold">Page Title</h1>
|
|
<p class="text-muted-foreground mt-2">Page description</p>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="pending" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<Skeleton v-for="i in 8" :key="i" class="h-64" />
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div v-else>
|
|
<!-- Page content here -->
|
|
</div>
|
|
</PageContainer>
|
|
</template>
|
|
```
|
|
|
|
### 2. For List Pages - Add Filtering
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
// Filter state from URL
|
|
const filters = computed(() => ({
|
|
status: route.query.status as string || '',
|
|
search: route.query.search as string || '',
|
|
page: parseInt(route.query.page as string) || 1
|
|
}))
|
|
|
|
// Fetch with filters
|
|
const { data, pending, refresh } = await useAsyncData(
|
|
`items-${JSON.stringify(filters.value)}`,
|
|
() => $fetch('/api/v1/items/', { params: filters.value }),
|
|
{ watch: [filters] }
|
|
)
|
|
|
|
// Update filters
|
|
function updateFilter(key: string, value: string) {
|
|
router.push({
|
|
query: { ...route.query, [key]: value || undefined, page: 1 }
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Filter Bar -->
|
|
<div class="flex gap-4 mb-6">
|
|
<Input
|
|
:model-value="filters.search"
|
|
@update:model-value="updateFilter('search', $event)"
|
|
placeholder="Search..."
|
|
/>
|
|
<Select
|
|
:model-value="filters.status"
|
|
@update:model-value="updateFilter('status', $event)"
|
|
>
|
|
<SelectOption value="">All</SelectOption>
|
|
<SelectOption value="operating">Operating</SelectOption>
|
|
<SelectOption value="closed">Closed</SelectOption>
|
|
</Select>
|
|
</div>
|
|
|
|
<!-- Results -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<ItemCard v-for="item in data?.results" :key="item.id" :item="item" />
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<Pagination
|
|
:current-page="filters.page"
|
|
:total-pages="Math.ceil((data?.count || 0) / 20)"
|
|
@page-change="updateFilter('page', $event.toString())"
|
|
/>
|
|
</template>
|
|
```
|
|
|
|
### 3. For Detail Pages - Dynamic Route
|
|
|
|
File: `frontend/pages/items/[slug].vue`
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
const route = useRoute()
|
|
const slug = route.params.slug as string
|
|
|
|
const { data: item, error } = await useAsyncData(
|
|
`item-${slug}`,
|
|
() => $fetch(`/api/v1/items/${slug}/`)
|
|
)
|
|
|
|
if (error.value) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
message: 'Item not found'
|
|
})
|
|
}
|
|
|
|
useSeoMeta({
|
|
title: `${item.value?.name} | ThrillWiki`,
|
|
description: item.value?.description,
|
|
})
|
|
</script>
|
|
```
|
|
|
|
### 4. For Form Pages
|
|
|
|
```vue
|
|
<script setup lang="ts">
|
|
import { z } from 'zod'
|
|
|
|
const schema = z.object({
|
|
name: z.string().min(1, 'Name is required'),
|
|
description: z.string().optional(),
|
|
})
|
|
|
|
const form = reactive({
|
|
name: '',
|
|
description: '',
|
|
})
|
|
|
|
const errors = ref<Record<string, string[]>>({})
|
|
const isSubmitting = ref(false)
|
|
|
|
async function handleSubmit() {
|
|
// Validate
|
|
const result = schema.safeParse(form)
|
|
if (!result.success) {
|
|
errors.value = result.error.flatten().fieldErrors
|
|
return
|
|
}
|
|
|
|
isSubmitting.value = true
|
|
try {
|
|
await $fetch('/api/v1/items/', {
|
|
method: 'POST',
|
|
body: form
|
|
})
|
|
await navigateTo('/items')
|
|
} catch (e) {
|
|
// Handle API errors
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
```
|
|
|
|
## Checklist
|
|
|
|
After creating the page, verify:
|
|
|
|
- [ ] Page renders without errors
|
|
- [ ] SEO meta tags are set
|
|
- [ ] Loading states display correctly
|
|
- [ ] Error states are handled
|
|
- [ ] Page is responsive (mobile, tablet, desktop)
|
|
- [ ] Keyboard navigation works
|
|
- [ ] Data fetches efficiently (no N+1 issues)
|
|
- [ ] URL parameters persist correctly (for list pages)
|
|
- [ ] Authentication is enforced (if required)
|
|
|
|
## Output
|
|
|
|
Report what was created:
|
|
```
|
|
Created: frontend/pages/[path].vue
|
|
Route: /[route]
|
|
Type: [list/detail/form/static]
|
|
Features: [list of features implemented]
|
|
```
|