mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:11:08 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"@csstools/normalize.css": "^12.1.1",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "^0.541.0",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.19",
|
||||
@@ -39,7 +40,6 @@
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^24.3.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.1",
|
||||
"@vitest/eslint-plugin": "^1.3.4",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
@@ -58,7 +58,7 @@
|
||||
"prettier": "3.6.2",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"typescript": "~5.9.2",
|
||||
"vite": "npm:rolldown-vite@^7.1.4",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-vue-devtools": "^8.0.1",
|
||||
"vitest": "^3.2.4",
|
||||
"vue-tsc": "^3.0.6"
|
||||
|
||||
5058
frontend/pnpm-lock.yaml
generated
Normal file
5058
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -99,6 +99,8 @@ const displayValue = computed(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.active-filter-chip {
|
||||
@apply transition-all duration-200;
|
||||
}
|
||||
|
||||
@@ -365,6 +365,8 @@ watch(
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.date-input {
|
||||
@apply transition-colors duration-200;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ defineEmits<{
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.filter-section {
|
||||
@apply border-b border-gray-100 dark:border-gray-700;
|
||||
}
|
||||
|
||||
@@ -219,6 +219,8 @@ if (typeof window !== "undefined") {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.preset-item {
|
||||
@apply transition-all duration-200;
|
||||
}
|
||||
|
||||
@@ -445,6 +445,8 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.ride-filter-sidebar {
|
||||
@apply flex flex-col h-full bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700;
|
||||
}
|
||||
|
||||
@@ -292,6 +292,8 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.search-filter {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
@@ -432,6 +432,8 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.search-input {
|
||||
@apply transition-colors duration-200;
|
||||
}
|
||||
|
||||
@@ -297,6 +297,8 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.select-input {
|
||||
@apply transition-colors duration-200;
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ export function useRideFiltering(initialFilters: RideFilters = {}) {
|
||||
const params: Record<string, string> = { q: query }
|
||||
if (role) params.role = role
|
||||
|
||||
const response = await api.client.get<CompanySearchResult[]>('/rides/api/search-companies/', params)
|
||||
const response = await api.client.get<CompanySearchResult[]>('/rides/api/search/companies/', params)
|
||||
return response
|
||||
} catch (err) {
|
||||
console.error('Error searching companies:', err)
|
||||
@@ -162,7 +162,7 @@ export function useRideFiltering(initialFilters: RideFilters = {}) {
|
||||
if (!query.trim()) return []
|
||||
|
||||
try {
|
||||
const response = await api.client.get<RideModelSearchResult[]>('/rides/api/search-ride-models/', { q: query })
|
||||
const response = await api.client.get<RideModelSearchResult[]>('/rides/api/search/ride-models/', { q: query })
|
||||
return response
|
||||
} catch (err) {
|
||||
console.error('Error searching ride models:', err)
|
||||
@@ -177,7 +177,7 @@ export function useRideFiltering(initialFilters: RideFilters = {}) {
|
||||
if (!query.trim()) return []
|
||||
|
||||
try {
|
||||
const response = await api.client.get<SearchSuggestion[]>('/rides/api/search-suggestions/', { q: query })
|
||||
const response = await api.client.get<SearchSuggestion[]>('/rides/api/search/suggestions/', { q: query })
|
||||
return response
|
||||
} catch (err) {
|
||||
console.error('Error getting search suggestions:', err)
|
||||
|
||||
@@ -145,7 +145,7 @@ export interface HistoryParams {
|
||||
}
|
||||
|
||||
// API configuration
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||
|
||||
// API response types
|
||||
interface ApiResponse<T> {
|
||||
@@ -323,28 +323,28 @@ export class ParksApi {
|
||||
if (params?.search) queryParams.search = params.search
|
||||
if (params?.ordering) queryParams.ordering = params.ordering
|
||||
|
||||
return this.client.get<ApiResponse<Park>>('/api/parks/', queryParams)
|
||||
return this.client.get<ApiResponse<Park>>('/parks/', queryParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single park by slug
|
||||
*/
|
||||
async getPark(slug: string): Promise<Park> {
|
||||
return this.client.get<Park>(`/api/parks/${slug}/`)
|
||||
return this.client.get<Park>(`/parks/${slug}/`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search parks
|
||||
*/
|
||||
async searchParks(query: string): Promise<SearchResponse<Park>> {
|
||||
return this.client.get<SearchResponse<Park>>('/api/parks/search/', { q: query })
|
||||
return this.client.get<SearchResponse<Park>>('/parks/', { search: query })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rides for a specific park
|
||||
*/
|
||||
async getParkRides(parkSlug: string): Promise<SearchResponse<Ride>> {
|
||||
return this.client.get<SearchResponse<Ride>>(`/api/parks/${parkSlug}/rides/`)
|
||||
return this.client.get<SearchResponse<Ride>>(`/parks/${parkSlug}/rides/`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -361,7 +361,7 @@ export class ParksApi {
|
||||
count: number
|
||||
days: number
|
||||
parks: Park[]
|
||||
}>('/api/parks/recent_changes/', params)
|
||||
}>('/parks/recent_changes/', params)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,7 +378,7 @@ export class ParksApi {
|
||||
count: number
|
||||
days: number
|
||||
parks: Park[]
|
||||
}>('/api/parks/recent_openings/', params)
|
||||
}>('/parks/recent_openings/', params)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -395,7 +395,7 @@ export class ParksApi {
|
||||
count: number
|
||||
days: number
|
||||
parks: Park[]
|
||||
}>('/api/parks/recent_closures/', params)
|
||||
}>('/parks/recent_closures/', params)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -412,7 +412,7 @@ export class ParksApi {
|
||||
count: number
|
||||
days: number
|
||||
parks: Park[]
|
||||
}>('/api/parks/recent_name_changes/', params)
|
||||
}>('/parks/recent_name_changes/', params)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ export class RidesApi {
|
||||
count: number
|
||||
days: number
|
||||
rides: Ride[]
|
||||
}>('/api/rides/recent_changes/', params)
|
||||
}>('/rides/recent_changes/', params)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -554,7 +554,7 @@ export class RidesApi {
|
||||
count: number
|
||||
days: number
|
||||
rides: Ride[]
|
||||
}>('/api/rides/recent_openings/', params)
|
||||
}>('/rides/recent_openings/', params)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,7 +571,7 @@ export class RidesApi {
|
||||
count: number
|
||||
days: number
|
||||
rides: Ride[]
|
||||
}>('/api/rides/recent_closures/', params)
|
||||
}>('/rides/recent_closures/', params)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -588,7 +588,7 @@ export class RidesApi {
|
||||
count: number
|
||||
days: number
|
||||
rides: Ride[]
|
||||
}>('/api/rides/recent_name_changes/', params)
|
||||
}>('/rides/recent_name_changes/', params)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -605,14 +605,14 @@ export class RidesApi {
|
||||
count: number
|
||||
days: number
|
||||
rides: Ride[]
|
||||
}>('/api/rides/recent_relocations/', params)
|
||||
}>('/rides/recent_relocations/', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filter options for ride filtering
|
||||
*/
|
||||
async getFilterOptions(): Promise<any> {
|
||||
return this.client.get('/rides/api/filter-options/')
|
||||
return this.client.get('/rides/filter-options/')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -621,21 +621,21 @@ export class RidesApi {
|
||||
async searchCompanies(query: string, role?: 'manufacturer' | 'designer' | 'both'): Promise<any[]> {
|
||||
const params: Record<string, string> = { q: query }
|
||||
if (role) params.role = role
|
||||
return this.client.get('/rides/api/search-companies/', params)
|
||||
return this.client.get('/rides/search-companies/', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search ride models for autocomplete
|
||||
*/
|
||||
async searchRideModels(query: string): Promise<any[]> {
|
||||
return this.client.get('/rides/api/search-ride-models/', { q: query })
|
||||
return this.client.get('/rides/search-ride-models/', { q: query })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search suggestions
|
||||
*/
|
||||
async getSearchSuggestions(query: string): Promise<any[]> {
|
||||
return this.client.get('/rides/api/search-suggestions/', { q: query })
|
||||
return this.client.get('/rides/search-suggestions/', { q: query })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -656,7 +656,7 @@ export class RidesApi {
|
||||
}
|
||||
})
|
||||
|
||||
return this.client.get<ApiResponse<Ride>>('/rides/api/', params)
|
||||
return this.client.get<ApiResponse<Ride>>('/rides/', params)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,7 +688,7 @@ export class AuthApi {
|
||||
* Login with username/email and password
|
||||
*/
|
||||
async login(credentials: LoginCredentials): Promise<AuthResponse> {
|
||||
const response = await this.client.post<AuthResponse>('/api/auth/login/', credentials)
|
||||
const response = await this.client.post<AuthResponse>('/auth/login/', credentials)
|
||||
if (response.token) {
|
||||
this.client.setAuthToken(response.token)
|
||||
}
|
||||
@@ -699,7 +699,7 @@ export class AuthApi {
|
||||
* Logout user
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
await this.client.post<void>('/api/auth/logout/')
|
||||
await this.client.post<void>('/auth/logout/')
|
||||
this.client.setAuthToken(null)
|
||||
}
|
||||
|
||||
@@ -707,7 +707,7 @@ export class AuthApi {
|
||||
* Register new user
|
||||
*/
|
||||
async signup(credentials: SignupCredentials): Promise<AuthResponse> {
|
||||
const response = await this.client.post<AuthResponse>('/api/auth/signup/', credentials)
|
||||
const response = await this.client.post<AuthResponse>('/auth/signup/', credentials)
|
||||
if (response.token) {
|
||||
this.client.setAuthToken(response.token)
|
||||
}
|
||||
@@ -718,28 +718,28 @@ export class AuthApi {
|
||||
* Get current user info
|
||||
*/
|
||||
async getCurrentUser(): Promise<User> {
|
||||
return this.client.get<User>('/api/auth/user/')
|
||||
return this.client.get<User>('/auth/user/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Request password reset
|
||||
*/
|
||||
async requestPasswordReset(data: PasswordResetRequest): Promise<{ detail: string }> {
|
||||
return this.client.post<{ detail: string }>('/api/auth/password/reset/', data)
|
||||
return this.client.post<{ detail: string }>('/auth/password/reset/', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change password
|
||||
*/
|
||||
async changePassword(data: PasswordChangeRequest): Promise<{ detail: string }> {
|
||||
return this.client.post<{ detail: string }>('/api/auth/password/change/', data)
|
||||
return this.client.post<{ detail: string }>('/auth/password/change/', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available social auth providers
|
||||
*/
|
||||
async getSocialProviders(): Promise<SocialAuthProvider[]> {
|
||||
return this.client.get<SocialAuthProvider[]>('/api/auth/providers/')
|
||||
return this.client.get<SocialAuthProvider[]>('/auth/providers/')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -774,7 +774,7 @@ export class HistoryApi {
|
||||
if (params?.model_type) queryParams.model_type = params.model_type
|
||||
if (params?.significance) queryParams.significance = params.significance
|
||||
|
||||
return this.client.get<UnifiedHistoryTimeline>('/api/history/timeline/', queryParams)
|
||||
return this.client.get<UnifiedHistoryTimeline>('/history/timeline/', queryParams)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -789,14 +789,14 @@ export class HistoryApi {
|
||||
if (params?.start_date) queryParams.start_date = params.start_date
|
||||
if (params?.end_date) queryParams.end_date = params.end_date
|
||||
|
||||
return this.client.get<HistoryEvent[]>(`/api/parks/${parkSlug}/history/`, queryParams)
|
||||
return this.client.get<HistoryEvent[]>(`/parks/${parkSlug}/history/`, queryParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complete park history with current state and summary
|
||||
*/
|
||||
async getParkHistoryDetail(parkSlug: string): Promise<ParkHistoryResponse> {
|
||||
return this.client.get<ParkHistoryResponse>(`/api/parks/${parkSlug}/history/detail/`)
|
||||
return this.client.get<ParkHistoryResponse>(`/parks/${parkSlug}/history/detail/`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -816,7 +816,7 @@ export class HistoryApi {
|
||||
if (params?.end_date) queryParams.end_date = params.end_date
|
||||
|
||||
return this.client.get<HistoryEvent[]>(
|
||||
`/api/parks/${parkSlug}/rides/${rideSlug}/history/`,
|
||||
`/parks/${parkSlug}/rides/${rideSlug}/history/`,
|
||||
queryParams
|
||||
)
|
||||
}
|
||||
@@ -826,7 +826,7 @@ export class HistoryApi {
|
||||
*/
|
||||
async getRideHistoryDetail(parkSlug: string, rideSlug: string): Promise<RideHistoryResponse> {
|
||||
return this.client.get<RideHistoryResponse>(
|
||||
`/api/parks/${parkSlug}/rides/${rideSlug}/history/detail/`
|
||||
`/parks/${parkSlug}/rides/${rideSlug}/history/detail/`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -894,14 +894,14 @@ export class TrendingApi {
|
||||
* Get trending content (rides, parks, reviews)
|
||||
*/
|
||||
async getTrendingContent(): Promise<TrendingResponse> {
|
||||
return this.client.get<TrendingResponse>('/api/trending/content/')
|
||||
return this.client.get<TrendingResponse>('/trending/content/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get new content (recently added, newly opened, upcoming)
|
||||
*/
|
||||
async getNewContent(): Promise<NewContentResponse> {
|
||||
return this.client.get<NewContentResponse>('/api/trending/new/')
|
||||
return this.client.get<NewContentResponse>('/trending/new/')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -983,35 +983,35 @@ export class RankingsApi {
|
||||
if (params?.park) queryParams.park = params.park
|
||||
if (params?.ordering) queryParams.ordering = params.ordering
|
||||
|
||||
return this.client.get<ApiResponse<RideRanking>>('/api/rankings/', queryParams)
|
||||
return this.client.get<ApiResponse<RideRanking>>('/rankings/', queryParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed ranking information for a specific ride
|
||||
*/
|
||||
async getRankingDetail(rideSlug: string): Promise<RideRankingDetail> {
|
||||
return this.client.get<RideRankingDetail>(`/api/rankings/${rideSlug}/`)
|
||||
return this.client.get<RideRankingDetail>(`/rankings/${rideSlug}/`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get historical ranking data for a specific ride (last 90 days)
|
||||
*/
|
||||
async getRankingHistory(rideSlug: string): Promise<RankingSnapshot[]> {
|
||||
return this.client.get<RankingSnapshot[]>(`/api/rankings/${rideSlug}/history/`)
|
||||
return this.client.get<RankingSnapshot[]>(`/rankings/${rideSlug}/history/`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get head-to-head comparison data for a ride
|
||||
*/
|
||||
async getHeadToHeadComparisons(rideSlug: string): Promise<HeadToHeadComparison[]> {
|
||||
return this.client.get<HeadToHeadComparison[]>(`/api/rankings/${rideSlug}/comparisons/`)
|
||||
return this.client.get<HeadToHeadComparison[]>(`/rankings/${rideSlug}/comparisons/`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system-wide ranking statistics
|
||||
*/
|
||||
async getRankingStatistics(): Promise<RankingStatistics> {
|
||||
return this.client.get<RankingStatistics>('/api/rankings/statistics/')
|
||||
return this.client.get<RankingStatistics>('/rankings/statistics/')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1031,7 +1031,7 @@ export class RankingsApi {
|
||||
comparisons_made: number
|
||||
duration: number
|
||||
timestamp: string
|
||||
}>('/api/rankings/calculate/', data)
|
||||
}>('/rankings/calculate/', data)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1119,7 +1119,7 @@ export class EntitySearchApi {
|
||||
if (params?.limit) queryParams.limit = params.limit.toString()
|
||||
if (params?.min_confidence) queryParams.min_confidence = params.min_confidence.toString()
|
||||
|
||||
return this.client.get<FuzzyMatchResult>('/api/entities/fuzzy-search/', queryParams)
|
||||
return this.client.get<FuzzyMatchResult>('/entities/fuzzy-search/', queryParams)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1135,7 +1135,7 @@ export class EntitySearchApi {
|
||||
if (context?.park_slug) data.park_slug = context.park_slug
|
||||
if (context?.path) data.path = context.path
|
||||
|
||||
return this.client.post<EntityNotFoundResponse>('/api/entities/not-found/', data)
|
||||
return this.client.post<EntityNotFoundResponse>('/entities/not-found/', data)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1152,7 +1152,7 @@ export class EntitySearchApi {
|
||||
if (params?.limit) queryParams.limit = params.limit.toString()
|
||||
if (params?.park_context) queryParams.park_context = params.park_context
|
||||
|
||||
return this.client.get<QuickSuggestionResponse>('/api/entities/quick-suggestions/', queryParams)
|
||||
return this.client.get<QuickSuggestionResponse>('/entities/quick-suggestions/', queryParams)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, watchEffect } from 'vue'
|
||||
import type {
|
||||
RideFilters,
|
||||
FilterOptions,
|
||||
@@ -37,6 +37,20 @@ export const useRideFilteringStore = defineStore('rideFiltering', () => {
|
||||
pendingFilters: {}
|
||||
})
|
||||
|
||||
// UI state for component compatibility
|
||||
const uiState = ref({
|
||||
sidebarVisible: false
|
||||
})
|
||||
|
||||
// Search state for component compatibility
|
||||
const searchState = ref({
|
||||
query: ''
|
||||
})
|
||||
|
||||
// Context state
|
||||
const contextType = ref<'global' | 'park'>('global')
|
||||
const contextValue = ref<string | null>(null)
|
||||
|
||||
// Results state
|
||||
const rides = ref<Ride[]>([])
|
||||
const totalCount = ref(0)
|
||||
@@ -44,11 +58,23 @@ export const useRideFilteringStore = defineStore('rideFiltering', () => {
|
||||
const hasNextPage = ref(false)
|
||||
const hasPreviousPage = ref(false)
|
||||
|
||||
// Search state
|
||||
// Search state - original for internal use
|
||||
const searchQuery = ref('')
|
||||
const searchSuggestions = ref<any[]>([])
|
||||
const showSuggestions = ref(false)
|
||||
|
||||
// Sync searchQuery with searchState.query for component compatibility
|
||||
watchEffect(() => {
|
||||
searchState.value.query = searchQuery.value
|
||||
})
|
||||
|
||||
// Sync searchState.query back to searchQuery
|
||||
watch(() => searchState.value.query, (newQuery) => {
|
||||
if (newQuery !== searchQuery.value) {
|
||||
searchQuery.value = newQuery
|
||||
}
|
||||
})
|
||||
|
||||
// Filter presets
|
||||
const savedPresets = ref<any[]>([])
|
||||
const currentPreset = ref<string | null>(null)
|
||||
@@ -116,6 +142,9 @@ export const useRideFilteringStore = defineStore('rideFiltering', () => {
|
||||
|
||||
const hasUnsavedChanges = computed(() => formState.value.hasChanges)
|
||||
|
||||
// Component compatibility - allFilters computed property
|
||||
const allFilters = computed(() => filters.value)
|
||||
|
||||
// Helper functions
|
||||
const getFilterLabel = (key: string): string => {
|
||||
const labels: Record<string, string> = {
|
||||
@@ -374,6 +403,27 @@ export const useRideFilteringStore = defineStore('rideFiltering', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Component compatibility methods
|
||||
const toggleSidebar = () => {
|
||||
uiState.value.sidebarVisible = !uiState.value.sidebarVisible
|
||||
}
|
||||
|
||||
const clearSearchQuery = () => {
|
||||
searchState.value.query = ''
|
||||
searchQuery.value = ''
|
||||
updateFilter('search', '')
|
||||
}
|
||||
|
||||
const setContext = (type: 'global' | 'park', value?: string) => {
|
||||
contextType.value = type
|
||||
contextValue.value = value || null
|
||||
|
||||
// Reset filters when context changes
|
||||
clearAllFilters()
|
||||
|
||||
console.log(`Context set to: ${type}${value ? ` (${value})` : ''}`)
|
||||
}
|
||||
|
||||
// Initialize presets from localStorage
|
||||
loadPresetsFromStorage()
|
||||
|
||||
@@ -395,12 +445,17 @@ export const useRideFilteringStore = defineStore('rideFiltering', () => {
|
||||
savedPresets,
|
||||
currentPreset,
|
||||
|
||||
// Component compatibility state
|
||||
uiState,
|
||||
searchState,
|
||||
|
||||
// Computed
|
||||
hasActiveFilters,
|
||||
activeFiltersCount,
|
||||
activeFiltersList,
|
||||
isFilterFormOpen,
|
||||
hasUnsavedChanges,
|
||||
allFilters,
|
||||
|
||||
// Actions
|
||||
updateFilter,
|
||||
@@ -436,6 +491,11 @@ export const useRideFilteringStore = defineStore('rideFiltering', () => {
|
||||
savePreset,
|
||||
loadPreset,
|
||||
deletePreset,
|
||||
loadPresetsFromStorage
|
||||
loadPresetsFromStorage,
|
||||
|
||||
// Component compatibility methods
|
||||
toggleSidebar,
|
||||
clearSearchQuery,
|
||||
setContext
|
||||
}
|
||||
})
|
||||
@@ -1,269 +1,392 @@
|
||||
<template>
|
||||
<div class="bg-gray-50 dark:bg-gray-900 min-h-screen">
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
{{ parkSlug ? `${parkName} Rides` : 'All Rides' }}
|
||||
</h1>
|
||||
<p v-if="parkSlug" class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
Explore all the thrilling rides and attractions at {{ parkName }}.
|
||||
</p>
|
||||
<p v-else class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
Discover amazing rides and attractions from theme parks around the world.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<!-- Filter Sidebar -->
|
||||
<RideFilterSidebar
|
||||
v-if="filterStore.uiState.sidebarVisible"
|
||||
class="w-80 flex-shrink-0 h-screen sticky top-0 overflow-y-auto"
|
||||
/>
|
||||
|
||||
<!-- Breadcrumb for park-specific rides -->
|
||||
<nav v-if="parkSlug" class="mb-6" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<li>
|
||||
<router-link to="/parks/" class="hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>Parks</router-link
|
||||
>
|
||||
</li>
|
||||
<li>/</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="`/parks/${parkSlug}/`"
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>{{ parkName }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>/</li>
|
||||
<li class="font-medium text-gray-900 dark:text-white">Rides</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-8">
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search rides..."
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
v-model="selectedCategory"
|
||||
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
<option value="roller-coaster">Roller Coasters</option>
|
||||
<option value="water-ride">Water Rides</option>
|
||||
<option value="family-ride">Family Rides</option>
|
||||
<option value="thrill-ride">Thrill Rides</option>
|
||||
<option value="dark-ride">Dark Rides</option>
|
||||
</select>
|
||||
<select
|
||||
v-model="sortBy"
|
||||
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="rating">Sort by Rating</option>
|
||||
<option value="height">Sort by Height</option>
|
||||
<option value="speed">Sort by Speed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="text-center py-12">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading rides...</p>
|
||||
</div>
|
||||
|
||||
<!-- Rides Grid -->
|
||||
<div v-else-if="filteredRides.length > 0" class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
v-for="ride in filteredRides"
|
||||
:key="ride.id"
|
||||
class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow cursor-pointer"
|
||||
@click="navigateToRide(ride)"
|
||||
>
|
||||
<!-- Ride Image -->
|
||||
<div class="h-48 bg-gradient-to-br from-purple-500 via-blue-500 to-teal-500 relative">
|
||||
<div class="absolute inset-0 bg-black bg-opacity-20"></div>
|
||||
<div class="absolute top-4 right-4">
|
||||
<span
|
||||
v-if="ride.featured"
|
||||
class="px-2 py-1 text-xs font-medium text-white bg-yellow-500 rounded-full"
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="text-center flex-1">
|
||||
<h1
|
||||
class="text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white mb-4"
|
||||
>
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
<div class="absolute bottom-4 left-4">
|
||||
<h3 class="text-white text-xl font-bold">{{ ride.name }}</h3>
|
||||
<p class="text-white text-sm opacity-90">{{ ride.park_name }}</p>
|
||||
{{ parkSlug ? `${parkName} Rides` : "All Rides" }}
|
||||
</h1>
|
||||
<p
|
||||
v-if="parkSlug"
|
||||
class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"
|
||||
>
|
||||
Explore all the thrilling rides and attractions at {{ parkName }}.
|
||||
</p>
|
||||
<p
|
||||
v-else
|
||||
class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"
|
||||
>
|
||||
Discover amazing rides and attractions from theme parks around the world.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Filter Sidebar Button -->
|
||||
<button
|
||||
@click="filterStore.toggleSidebar()"
|
||||
class="ml-4 p-2 rounded-lg bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl transition-shadow border border-gray-200 dark:border-gray-700"
|
||||
:class="{
|
||||
'text-blue-600 dark:text-blue-400': filterStore.uiState.sidebarVisible,
|
||||
}"
|
||||
>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Ride Info -->
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center">
|
||||
<svg class="h-4 w-4 text-yellow-500 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<!-- Breadcrumb for park-specific rides -->
|
||||
<nav v-if="parkSlug" class="mb-6" aria-label="Breadcrumb">
|
||||
<ol
|
||||
class="flex items-center space-x-2 text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<li>
|
||||
<router-link
|
||||
to="/parks/"
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>Parks</router-link
|
||||
>
|
||||
</li>
|
||||
<li>/</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="`/parks/${parkSlug}/`"
|
||||
class="hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>{{ parkName }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>/</li>
|
||||
<li class="font-medium text-gray-900 dark:text-white">Rides</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Quick Filter Bar (when sidebar is hidden) -->
|
||||
<div
|
||||
v-if="!filterStore.uiState.sidebarVisible"
|
||||
class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-8"
|
||||
>
|
||||
<div class="flex flex-col md:flex-row gap-4 items-center">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
v-model="filterStore.searchState.query"
|
||||
type="text"
|
||||
placeholder="Search rides..."
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@click="filterStore.toggleSidebar()"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ ride.average_rating ? ride.average_rating.toFixed(1) : 'N/A' }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-medium text-blue-800 bg-blue-100 dark:bg-blue-700 dark:text-blue-50 rounded-full"
|
||||
>
|
||||
{{ ride.category_display }}
|
||||
</span>
|
||||
More Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-2">
|
||||
{{ ride.description }}
|
||||
</p>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div v-if="ride.height" class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Height:</span> {{ ride.height }}ft
|
||||
<!-- Active Filters Display -->
|
||||
<div v-if="hasActiveFilters" class="mb-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-medium text-gray-900 dark:text-white">Active Filters</h3>
|
||||
<button
|
||||
@click="filterStore.clearAllFilters()"
|
||||
class="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="ride.speed" class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Speed:</span> {{ ride.speed }}mph
|
||||
</div>
|
||||
<div v-if="ride.length" class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Length:</span> {{ ride.length }}ft
|
||||
</div>
|
||||
<div v-if="ride.duration" class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Duration:</span> {{ ride.duration }}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- Search filter chip -->
|
||||
<div
|
||||
v-if="filterStore.searchState.query"
|
||||
class="flex items-center gap-1 px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-sm"
|
||||
>
|
||||
<span>Search: "{{ filterStore.searchState.query }}"</span>
|
||||
<button
|
||||
@click="filterStore.clearSearchQuery()"
|
||||
class="ml-1 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200"
|
||||
>
|
||||
<svg class="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Other active filter chips will be handled by the filter store -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="text-center py-12">
|
||||
<svg
|
||||
class="h-16 w-16 mx-auto text-gray-300 dark:text-gray-600 mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
></path>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">No rides found</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
Try adjusting your search or filter criteria.
|
||||
</p>
|
||||
<!-- Results Summary -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
<span v-if="loading">Loading rides...</span>
|
||||
<span v-else-if="error" class="text-red-600 dark:text-red-400">{{
|
||||
error
|
||||
}}</span>
|
||||
<span v-else>
|
||||
{{ totalCount }} {{ totalCount === 1 ? "ride" : "rides" }} found
|
||||
<span v-if="hasActiveFilters">(filtered)</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- Sort Controls -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-400">Sort by:</label>
|
||||
<select
|
||||
v-model="filterStore.filters.ordering"
|
||||
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm"
|
||||
>
|
||||
<option value="name">Name</option>
|
||||
<option value="-average_rating">Rating (High to Low)</option>
|
||||
<option value="-height">Height (High to Low)</option>
|
||||
<option value="-speed">Speed (High to Low)</option>
|
||||
<option value="park_name">Park Name</option>
|
||||
<option value="category">Category</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="text-center py-12">
|
||||
<div
|
||||
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"
|
||||
></div>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading rides...</p>
|
||||
</div>
|
||||
|
||||
<!-- Rides Grid -->
|
||||
<div
|
||||
v-else-if="rides.length > 0"
|
||||
class="grid gap-6 md:grid-cols-2 lg:grid-cols-3"
|
||||
>
|
||||
<div
|
||||
v-for="ride in rides"
|
||||
:key="ride.id"
|
||||
class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow cursor-pointer"
|
||||
@click="navigateToRide(ride)"
|
||||
>
|
||||
<!-- Ride Image -->
|
||||
<div
|
||||
class="h-48 bg-gradient-to-br from-purple-500 via-blue-500 to-teal-500 relative"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black bg-opacity-20"></div>
|
||||
<div class="absolute top-4 right-4">
|
||||
<span
|
||||
v-if="ride.featured"
|
||||
class="px-2 py-1 text-xs font-medium text-white bg-yellow-500 rounded-full"
|
||||
>
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
<div class="absolute bottom-4 left-4">
|
||||
<h3 class="text-white text-xl font-bold">{{ ride.name }}</h3>
|
||||
<p class="text-white text-sm opacity-90">{{ ride.park_name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ride Info -->
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center">
|
||||
<svg
|
||||
class="h-4 w-4 text-yellow-500 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ ride.average_rating ? ride.average_rating.toFixed(1) : "N/A" }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-medium text-blue-800 bg-blue-100 dark:bg-blue-700 dark:text-blue-50 rounded-full"
|
||||
>
|
||||
{{ ride.category_display }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-2">
|
||||
{{ ride.description }}
|
||||
</p>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div v-if="ride.height" class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Height:</span> {{ ride.height }}ft
|
||||
</div>
|
||||
<div v-if="ride.speed" class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Speed:</span> {{ ride.speed }}mph
|
||||
</div>
|
||||
<div v-if="ride.length" class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Length:</span> {{ ride.length }}ft
|
||||
</div>
|
||||
<div v-if="ride.duration" class="text-gray-600 dark:text-gray-400">
|
||||
<span class="font-medium">Duration:</span> {{ ride.duration }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="text-center py-12">
|
||||
<svg
|
||||
class="h-16 w-16 mx-auto text-gray-300 dark:text-gray-600 mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
></path>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
No rides found
|
||||
</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">
|
||||
Try adjusting your search or filter criteria.
|
||||
</p>
|
||||
<button
|
||||
v-if="hasActiveFilters"
|
||||
@click="filterStore.clearAllFilters()"
|
||||
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Clear All Filters
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Load More Button -->
|
||||
<div v-if="rides.length > 0 && !loading" class="text-center mt-8">
|
||||
<button
|
||||
v-if="hasNextPage"
|
||||
@click="loadMore"
|
||||
:disabled="loading"
|
||||
class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{{ loading ? "Loading..." : "Load More Rides" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { api } from '@/services/api'
|
||||
import type { Ride } from '@/types'
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useRideFiltering, useParkRideFiltering } from "@/composables/useRideFiltering";
|
||||
import { useRideFilteringStore } from "@/stores/rideFiltering";
|
||||
import RideFilterSidebar from "@/components/filters/RideFilterSidebar.vue";
|
||||
import type { Ride } from "@/types";
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const rides = ref<Ride[]>([])
|
||||
const searchQuery = ref('')
|
||||
const selectedCategory = ref('')
|
||||
const sortBy = ref('name')
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// Get park context from route params
|
||||
const parkSlug = computed(() => route.params.parkSlug as string)
|
||||
const parkName = ref('')
|
||||
const parkSlug = computed(() => route.params.parkSlug as string);
|
||||
const parkName = ref("");
|
||||
|
||||
const filteredRides = computed(() => {
|
||||
let result = rides.value
|
||||
// Initialize filtering store
|
||||
const filterStore = useRideFilteringStore();
|
||||
|
||||
// Apply search filter
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(ride) =>
|
||||
ride.name.toLowerCase().includes(query) ||
|
||||
ride.description.toLowerCase().includes(query) ||
|
||||
ride.park_name.toLowerCase().includes(query),
|
||||
)
|
||||
}
|
||||
// Use appropriate composable based on context
|
||||
const rideFiltering = parkSlug.value
|
||||
? useParkRideFiltering(parkSlug.value, {})
|
||||
: useRideFiltering({});
|
||||
|
||||
// Apply category filter
|
||||
if (selectedCategory.value) {
|
||||
result = result.filter((ride) => ride.category === selectedCategory.value)
|
||||
}
|
||||
const {
|
||||
isLoading: loading,
|
||||
error,
|
||||
rides,
|
||||
totalCount,
|
||||
hasNextPage,
|
||||
hasActiveFilters,
|
||||
fetchRides,
|
||||
loadMore,
|
||||
fetchFilterOptions,
|
||||
} = rideFiltering;
|
||||
|
||||
// Apply sorting
|
||||
result.sort((a, b) => {
|
||||
switch (sortBy.value) {
|
||||
case 'rating':
|
||||
return (b.average_rating || 0) - (a.average_rating || 0)
|
||||
case 'height':
|
||||
return (b.height || 0) - (a.height || 0)
|
||||
case 'speed':
|
||||
return (b.speed || 0) - (a.speed || 0)
|
||||
default:
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const fetchRides = async () => {
|
||||
// Initialize filter store with context
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
let response
|
||||
|
||||
// Set context in store
|
||||
if (parkSlug.value) {
|
||||
// Fetch rides for specific park
|
||||
response = await api.parks.getParkRides(parkSlug.value)
|
||||
filterStore.setContext("park", parkSlug.value);
|
||||
|
||||
// Also fetch park info to get the park name
|
||||
// Fetch park info to get the park name
|
||||
try {
|
||||
const parkData = await api.parks.getPark(parkSlug.value)
|
||||
parkName.value = parkData.name
|
||||
const { api } = await import("@/services/api");
|
||||
const parkData = await api.parks.getPark(parkSlug.value);
|
||||
parkName.value = parkData.name;
|
||||
} catch (err) {
|
||||
console.warn('Could not fetch park name:', err)
|
||||
console.warn("Could not fetch park name:", err);
|
||||
}
|
||||
|
||||
rides.value = response.results
|
||||
} else {
|
||||
// Fetch all rides
|
||||
response = await api.rides.getRides({
|
||||
search: searchQuery.value || undefined,
|
||||
ordering: sortBy.value === 'rating' ? '-average_rating' : sortBy.value,
|
||||
})
|
||||
|
||||
rides.value = response.results
|
||||
filterStore.setContext("global");
|
||||
}
|
||||
|
||||
// Load filter options and initial rides
|
||||
await Promise.all([fetchFilterOptions(), fetchRides()]);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch rides:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
console.error("Failed to initialize ride list:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for filter changes in store and update composable
|
||||
watch(
|
||||
() => filterStore.allFilters,
|
||||
(newFilters) => {
|
||||
// Update the composable's filters
|
||||
rideFiltering.updateFilters(newFilters);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const navigateToRide = (ride: Ride) => {
|
||||
router.push(`/parks/${ride.parkSlug}/rides/${ride.slug}/`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRides()
|
||||
})
|
||||
router.push(`/parks/${ride.parkSlug}/rides/${ride.slug}/`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
@@ -10,7 +9,6 @@ import tailwindcss from '@tailwindcss/vite'
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
vueDevTools(),
|
||||
tailwindcss(),
|
||||
],
|
||||
@@ -22,11 +20,12 @@ export default defineConfig(({ mode }) => ({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
target: 'http://127.0.0.1:8000/api/v1',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
ws: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api/v1')
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
// Frontend calls /api/parks/ -> rewrites to /parks/ -> target adds /api/v1 -> /api/v1/parks/
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user