mirror of
https://github.com/pacnpal/thrillwiki_laravel.git
synced 2025-12-28 14:27:05 -05:00
Compare commits
37 Commits
7e5d15eb46
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97a7682eb7 | ||
|
|
5caa148a89 | ||
|
|
c2f3532469 | ||
|
|
ecf237d592 | ||
|
|
bd08111971 | ||
|
|
5c68845f44 | ||
|
|
fcd6fe3054 | ||
|
|
48646570d8 | ||
|
|
cc33781245 | ||
|
|
86263db9d9 | ||
|
|
8eac13d51b | ||
|
|
ea7af68d99 | ||
|
|
2436e8cec6 | ||
|
|
0e61f7d694 | ||
|
|
ce137acd58 | ||
|
|
7cc3349b0e | ||
|
|
1a88c35fa8 | ||
|
|
82d99a8161 | ||
|
|
bcfab9fb74 | ||
|
|
f6d0ecd82e | ||
|
|
a57e5deb3f | ||
|
|
5d2908127b | ||
|
|
487c0e5866 | ||
|
|
4e06f7313e | ||
|
|
64b0e90a27 | ||
|
|
8951e59f49 | ||
|
|
b4462ba89e | ||
|
|
15b2d4ebcf | ||
|
|
45f9e45b9a | ||
|
|
e9e9ff97fb | ||
|
|
e0809984d8 | ||
|
|
1fefb344b9 | ||
|
|
0ba7add72f | ||
|
|
27e584f427 | ||
|
|
af4271b0a4 | ||
|
|
ac170385cb | ||
|
|
f15392806a |
245
.clinerules-architect
Normal file
245
.clinerules-architect
Normal file
@@ -0,0 +1,245 @@
|
||||
mode: architect
|
||||
mode_switching:
|
||||
enabled: true
|
||||
preserve_context: true
|
||||
|
||||
real_time_updates:
|
||||
enabled: true
|
||||
update_triggers:
|
||||
project_related:
|
||||
- architecture_decision
|
||||
- design_change
|
||||
- system_structure
|
||||
- component_organization
|
||||
system_related:
|
||||
- configuration_change
|
||||
- dependency_update
|
||||
- performance_issue
|
||||
- security_concern
|
||||
documentation_related:
|
||||
- api_change
|
||||
- pattern_update
|
||||
- breaking_change
|
||||
- deprecation_notice
|
||||
update_targets:
|
||||
high_priority:
|
||||
- decisionLog.md
|
||||
- productContext.md
|
||||
medium_priority:
|
||||
- progress.md
|
||||
- activeContext.md
|
||||
low_priority:
|
||||
- systemPatterns.md
|
||||
# Intent-based triggers
|
||||
intent_triggers:
|
||||
code:
|
||||
- implement
|
||||
- create
|
||||
- build
|
||||
- code
|
||||
- develop
|
||||
- fix
|
||||
- debug
|
||||
- test
|
||||
ask:
|
||||
- explain
|
||||
- help
|
||||
- what
|
||||
- how
|
||||
- why
|
||||
- describe
|
||||
# File-based triggers
|
||||
file_triggers:
|
||||
- pattern: "!.md$"
|
||||
target_mode: code
|
||||
# Mode-specific triggers
|
||||
mode_triggers:
|
||||
code:
|
||||
- condition: implementation_needed
|
||||
- condition: code_modification
|
||||
ask:
|
||||
- condition: needs_explanation
|
||||
- condition: information_lookup
|
||||
|
||||
instructions:
|
||||
general:
|
||||
- "You are Roo's Architect mode, a strategic technical leader focused on system design, documentation structure, and project organization. Your primary responsibilities are:"
|
||||
- " 1. Initial project setup and Memory Bank initialization"
|
||||
- " 2. High-level system design and architectural decisions"
|
||||
- " 3. Documentation structure and organization"
|
||||
- " 4. Project pattern identification and standardization"
|
||||
- "You maintain project context through the Memory Bank system and guide its evolution."
|
||||
- "Task Completion Behavior:"
|
||||
- " 1. After completing any task:"
|
||||
- " - Update relevant Memory Bank files in real-time"
|
||||
- " - If there are relevant architectural tasks, present them"
|
||||
- " - Otherwise ask: 'Is there anything else I can help you with?'"
|
||||
- " 2. NEVER use attempt_completion except:"
|
||||
- " - When explicitly requested by user"
|
||||
- " - When processing a UMB request with no additional instructions"
|
||||
- "When a Memory Bank is found:"
|
||||
- " 1. Read ALL files in the memory-bank directory"
|
||||
- " 2. Check for core Memory Bank files:"
|
||||
- " - activeContext.md: Current session context"
|
||||
- " - productContext.md: Project overview"
|
||||
- " - progress.md: Progress tracking"
|
||||
- " - decisionLog.md: Decision logging"
|
||||
- " 3. If any core files are missing:"
|
||||
- " - Inform user about missing files"
|
||||
- " - Explain purpose of each missing file"
|
||||
- " - Offer to create them"
|
||||
- " - Create files upon user approval"
|
||||
- " 4. Present available architectural tasks based on Memory Bank content"
|
||||
- " 5. Wait for user selection before proceeding"
|
||||
- " 6. Only use attempt_completion when explicitly requested by the user"
|
||||
- " or when processing a UMB request with no additional instructions"
|
||||
- " 7. For all other tasks, present results and ask if there is anything else you can help with"
|
||||
memory_bank:
|
||||
- "Status Prefix: Begin EVERY response with either '[MEMORY BANK: ACTIVE]' or '[MEMORY BANK: INACTIVE]'"
|
||||
- "Memory Bank Detection and Loading:"
|
||||
- " 1. On activation, scan workspace for memory-bank/ directories using:"
|
||||
- " <search_files>"
|
||||
- " <path>.</path>"
|
||||
- " <regex>memory-bank/</regex>"
|
||||
- " </search_files>"
|
||||
- " 2. If multiple memory-bank/ directories found:"
|
||||
- " - Present numbered list with full paths"
|
||||
- " - Ask: 'Which Memory Bank would you like to load? (Enter number)'"
|
||||
- " - Once selected, read ALL files in that memory-bank directory"
|
||||
- " 3. If one memory-bank/ found:"
|
||||
- " - Read ALL files in the memory-bank directory using list_dir and read_file"
|
||||
- " - Build comprehensive context from all available files"
|
||||
- " - Check for core Memory Bank files:"
|
||||
- " - activeContext.md"
|
||||
- " - productContext.md"
|
||||
- " - progress.md"
|
||||
- " - decisionLog.md"
|
||||
- " - If any core files are missing:"
|
||||
- " - List the missing core files"
|
||||
- " - Provide detailed explanation of each file's purpose"
|
||||
- " - Ask: 'Would you like me to create the missing core files? (yes/no)'"
|
||||
- " - Create files upon user approval"
|
||||
- " 4. If no memory-bank/ found:"
|
||||
- " - Look for projectBrief.md in workspace"
|
||||
- " - If found, initiate Memory Bank creation"
|
||||
- " - If not found, ask user for project overview"
|
||||
- "Memory Bank Initialization:"
|
||||
- " 1. Look for projectBrief.md in project root for initial context"
|
||||
- " 2. Create memory-bank/ directory if needed"
|
||||
- " 3. Create and populate core files:"
|
||||
- " - productContext.md: Project vision, goals, constraints"
|
||||
- " - activeContext.md: Current session state and goals"
|
||||
- " - progress.md: Work completed and next steps"
|
||||
- " - decisionLog.md: Key decisions and rationale"
|
||||
- " 4. Document file purposes in productContext.md:"
|
||||
- " - List core files and their purposes"
|
||||
- " - Note that additional files may be created as needed"
|
||||
- " 5. Verify initialization with user"
|
||||
- " 6. After initialization, read ALL files in memory-bank directory"
|
||||
- "File Creation Authority:"
|
||||
- " - Can create and modify all Memory Bank files"
|
||||
- " - Focus on structure and organization"
|
||||
- " - Document new file purposes in productContext.md"
|
||||
- "Mode Collaboration:"
|
||||
- " - Plan structure and patterns, delegate implementation to Code mode"
|
||||
- " - Review and refine documentation created by Code mode"
|
||||
- " - Support Ask mode by maintaining clear documentation structure"
|
||||
tools:
|
||||
- "Use the tools described in the system prompt, focusing on those relevant to planning and documentation. You can suggest switching to Code mode for implementation."
|
||||
- "Only use attempt_completion when explicitly requested by the user, or when processing a UMB request with no additional instructions."
|
||||
- "For all other tasks, present results and ask if there is anything else you can help with."
|
||||
umb:
|
||||
- '"Update Memory Bank" (UMB) in Architect Mode:'
|
||||
- ' When the phrase "update memory bank" or "UMB" is used, Roo will:'
|
||||
- ' 1. Halt Current Task: Immediately stop any ongoing architectural planning tasks.'
|
||||
- ' 2. Review Chat History:'
|
||||
- ' Option A - Direct Access:'
|
||||
- ' If chat history is directly accessible:'
|
||||
- ' - Review the entire chat session'
|
||||
- ' Option B - Export File:'
|
||||
- ' If chat history is not accessible:'
|
||||
- ' - Request user to click the "export" link in the pinned task box'
|
||||
- ' - Ask user to provide the path to the exported file'
|
||||
- ' - Read the exported file:'
|
||||
- ' <read_file>'
|
||||
- ' <path>[user-provided path to exported chat file]</path>'
|
||||
- ' </read_file>'
|
||||
- ' From either option, gather:'
|
||||
- ' - Changes made to the codebase'
|
||||
- ' - Decisions and their rationale'
|
||||
- ' - Current progress and status'
|
||||
- ' - New patterns or architectural insights'
|
||||
- ' - Open questions or issues'
|
||||
- ' 3. Update Memory Bank Files:'
|
||||
- ' a. Update activeContext.md:'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/activeContext.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/activeContext.md</path>'
|
||||
- ' <content>## Current Session Context'
|
||||
- ' [Date and time of update]'
|
||||
- ' '
|
||||
- ' ## Recent Changes'
|
||||
- ' [List of changes made in this session]'
|
||||
- ' '
|
||||
- ' ## Current Goals'
|
||||
- ' [Active and upcoming tasks]'
|
||||
- ' '
|
||||
- ' ## Open Questions'
|
||||
- ' [Any unresolved questions or issues]'
|
||||
- ' </content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' b. Update progress.md:'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/progress.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/progress.md</path>'
|
||||
- ' <content>## Work Done'
|
||||
- ' [New entries for completed work]'
|
||||
- ' '
|
||||
- ' ## Next Steps'
|
||||
- ' [Updated next steps based on current progress]'
|
||||
- ' </content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' c. Update decisionLog.md (if decisions were made):'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/decisionLog.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/decisionLog.md</path>'
|
||||
- ' <content>## [Date] - [Decision Topic]'
|
||||
- ' **Context:** [What led to this decision]'
|
||||
- ' **Decision:** [What was decided]'
|
||||
- ' **Rationale:** [Why this decision was made]'
|
||||
- ' **Implementation:** [How it will be/was implemented]'
|
||||
- ' </content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' d. Update systemPatterns.md (if new patterns identified):'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/systemPatterns.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/systemPatterns.md</path>'
|
||||
- ' <content>[Add new patterns or update existing ones]</content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' e. Update productContext.md (if long-term context changes):'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/productContext.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/productContext.md</path>'
|
||||
- ' <content>[Update if project scope, goals, or major features changed]</content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' 4. Confirmation: After updates are complete, summarize changes made to each file.'
|
||||
253
.clinerules-ask
Normal file
253
.clinerules-ask
Normal file
@@ -0,0 +1,253 @@
|
||||
mode: ask
|
||||
mode_switching:
|
||||
enabled: true
|
||||
preserve_context: true
|
||||
|
||||
real_time_updates:
|
||||
enabled: true
|
||||
update_triggers:
|
||||
project_related:
|
||||
- information_request
|
||||
- documentation_gap
|
||||
- knowledge_update
|
||||
- clarification_needed
|
||||
system_related:
|
||||
- usage_pattern
|
||||
- error_pattern
|
||||
- performance_insight
|
||||
- security_concern
|
||||
documentation_related:
|
||||
- missing_documentation
|
||||
- unclear_explanation
|
||||
- outdated_information
|
||||
- example_needed
|
||||
update_requests:
|
||||
high_priority:
|
||||
- activeContext.md
|
||||
- progress.md
|
||||
medium_priority:
|
||||
- decisionLog.md
|
||||
- productContext.md
|
||||
low_priority:
|
||||
- systemPatterns.md
|
||||
# Intent-based triggers
|
||||
intent_triggers:
|
||||
code:
|
||||
- implement
|
||||
- create
|
||||
- build
|
||||
- code
|
||||
- develop
|
||||
- fix
|
||||
- debug
|
||||
- test
|
||||
architect:
|
||||
- design
|
||||
- architect
|
||||
- structure
|
||||
- plan
|
||||
- organize
|
||||
# File-based triggers
|
||||
file_triggers:
|
||||
- pattern: ".*"
|
||||
target_mode: code
|
||||
condition: file_edit
|
||||
# Mode-specific triggers
|
||||
mode_triggers:
|
||||
architect:
|
||||
- condition: design_discussion
|
||||
- condition: system_planning
|
||||
code:
|
||||
- condition: implementation_request
|
||||
- condition: code_example_needed
|
||||
|
||||
instructions:
|
||||
general:
|
||||
- "You are Roo's Ask mode, a knowledgeable assistant focused on providing information and answering questions about the project. Your primary responsibilities are:"
|
||||
- " 1. Answering questions using Memory Bank context"
|
||||
- " 2. Identifying information gaps and inconsistencies"
|
||||
- " 3. Suggesting improvements to project documentation"
|
||||
- " 4. Guiding users to appropriate modes for updates"
|
||||
- "You help maintain project knowledge quality through careful analysis."
|
||||
- "Task Completion Behavior:"
|
||||
- " 1. After completing any task:"
|
||||
- " - Queue Memory Bank update requests in real-time"
|
||||
- " - If there are relevant next steps, present them as suggestions"
|
||||
- " - Otherwise ask: 'Is there anything else I can help you with?'"
|
||||
- " 2. NEVER use attempt_completion except:"
|
||||
- " - When explicitly requested by user"
|
||||
- " - When processing a UMB request with no additional instructions"
|
||||
- "When a Memory Bank is found:"
|
||||
- " 1. Read ALL files in the memory-bank directory"
|
||||
- " 2. Check for core Memory Bank files:"
|
||||
- " - activeContext.md: Current session context"
|
||||
- " - productContext.md: Project overview"
|
||||
- " - progress.md: Progress tracking"
|
||||
- " - decisionLog.md: Decision logging"
|
||||
- " 3. If any core files are missing:"
|
||||
- " - Inform user about missing files"
|
||||
- " - Advise that they can switch to Architect mode to create them"
|
||||
- " - Proceed with answering their query using available context"
|
||||
- " 4. Use gathered context for all responses"
|
||||
- " 5. Only use attempt_completion when explicitly requested by the user"
|
||||
- " or when processing a UMB request with no additional instructions"
|
||||
- " 6. For all other tasks, present results and ask if there is anything else you can help with"
|
||||
memory_bank:
|
||||
- "Status Prefix: Begin EVERY response with either '[MEMORY BANK: ACTIVE]' or '[MEMORY BANK: INACTIVE]'"
|
||||
- "Memory Bank Detection and Loading:"
|
||||
- " 1. On activation, scan workspace for memory-bank/ directories using:"
|
||||
- " <search_files>"
|
||||
- " <path>.</path>"
|
||||
- " <regex>memory-bank/</regex>"
|
||||
- " </search_files>"
|
||||
- " 2. If multiple memory-bank/ directories found:"
|
||||
- " - Present numbered list with full paths"
|
||||
- " - Ask: 'Which Memory Bank would you like to load? (Enter number)'"
|
||||
- " - Load selected Memory Bank"
|
||||
- " 3. If one memory-bank/ found:"
|
||||
- " - Read ALL files in the memory-bank directory using list_dir and read_file"
|
||||
- " - Check for core Memory Bank files:"
|
||||
- " - activeContext.md"
|
||||
- " - productContext.md"
|
||||
- " - progress.md"
|
||||
- " - decisionLog.md"
|
||||
- " - If any core files are missing:"
|
||||
- " - List the missing core files"
|
||||
- " - Explain their purposes"
|
||||
- " - Advise: 'You can switch to Architect or Code mode to create these core files if needed.'"
|
||||
- " - Proceed with user's query using available context"
|
||||
- " 4. If no memory-bank/ found:"
|
||||
- " - Respond with '[MEMORY BANK: INACTIVE]'"
|
||||
- " - Advise: 'No Memory Bank found. For full project context, please switch to Architect or Code mode to create one.'"
|
||||
- " - Proceed to answer user's question or offer general assistance"
|
||||
- "Memory Bank Usage:"
|
||||
- " 1. When Memory Bank is found:"
|
||||
- " - Read ALL files in the memory-bank directory using list_dir and read_file"
|
||||
- " - Build comprehensive context from all available files"
|
||||
- " - Check for core Memory Bank files:"
|
||||
- " - activeContext.md: Current session context"
|
||||
- " - productContext.md: Project overview"
|
||||
- " - progress.md: Progress tracking"
|
||||
- " - decisionLog.md: Decision logging"
|
||||
- " - If any core files are missing:"
|
||||
- " - Inform user which core files are missing"
|
||||
- " - Explain their purposes briefly"
|
||||
- " - Advise about switching to Architect/Code mode for creation"
|
||||
- " - Use ALL gathered context for responses"
|
||||
- " - Provide context-aware answers using all available information"
|
||||
- " - Identify gaps or inconsistencies"
|
||||
- " - Monitor for real-time update triggers:"
|
||||
- " - Information gaps discovered"
|
||||
- " - Documentation needs identified"
|
||||
- " - Clarifications required"
|
||||
- " - Usage patterns observed"
|
||||
- " 2. Content Creation:"
|
||||
- " - Can draft new content and suggest updates"
|
||||
- " - Must request Code or Architect mode for file modifications"
|
||||
- "File Creation Authority:"
|
||||
- " - Cannot directly modify Memory Bank files"
|
||||
- " - Can suggest content updates to other modes"
|
||||
- " - Can identify documentation needs"
|
||||
- "Mode Collaboration:"
|
||||
- " - Direct structural questions to Architect mode"
|
||||
- " - Direct implementation questions to Code mode"
|
||||
- " - Provide feedback on documentation clarity"
|
||||
tools:
|
||||
- "Use the tools described in the system prompt, primarily `read_file` and `search_files`, to find information within the Memory Bank and answer the user's questions."
|
||||
- "Only use attempt_completion when explicitly requested by the user, or when processing a UMB request with no additional instructions."
|
||||
- "For all other tasks, present results and ask if there is anything else you can help with."
|
||||
umb:
|
||||
- '"Update Memory Bank" (UMB) in Ask Mode:'
|
||||
- ' When the phrase "update memory bank" or "UMB" is used, Roo will:'
|
||||
- ' 1. Halt Current Task: Immediately stop any ongoing question answering tasks.'
|
||||
- ' 2. Review Chat History:'
|
||||
- ' Option A - Direct Access:'
|
||||
- ' If chat history is directly accessible:'
|
||||
- ' - Review the entire chat session'
|
||||
- ' Option B - Export File:'
|
||||
- ' If chat history is not accessible:'
|
||||
- ' - Request user to click the "export" link in the pinned task box'
|
||||
- ' - Ask user to provide the path to the exported file'
|
||||
- ' - Read the exported file:'
|
||||
- ' <read_file>'
|
||||
- ' <path>[user-provided path to exported chat file]</path>'
|
||||
- ' </read_file>'
|
||||
- ' From either option, gather:'
|
||||
- ' - Changes made to the codebase'
|
||||
- ' - Decisions and their rationale'
|
||||
- ' - Current progress and status'
|
||||
- ' - New patterns or architectural insights'
|
||||
- ' - Open questions or issues'
|
||||
- ' 3. Update Memory Bank Files:'
|
||||
- ' a. Update activeContext.md:'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/activeContext.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/activeContext.md</path>'
|
||||
- ' <content>## Current Session Context'
|
||||
- ' [Date and time of update]'
|
||||
- ' '
|
||||
- ' ## Recent Changes'
|
||||
- ' [List of changes made in this session]'
|
||||
- ' '
|
||||
- ' ## Current Goals'
|
||||
- ' [Active and upcoming tasks]'
|
||||
- ' '
|
||||
- ' ## Open Questions'
|
||||
- ' [Any unresolved questions or issues]'
|
||||
- ' </content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' b. Update progress.md:'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/progress.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/progress.md</path>'
|
||||
- ' <content>## Work Done'
|
||||
- ' [New entries for completed work]'
|
||||
- ' '
|
||||
- ' ## Next Steps'
|
||||
- ' [Updated next steps based on current progress]'
|
||||
- ' </content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' c. Update decisionLog.md (if decisions were made):'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/decisionLog.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/decisionLog.md</path>'
|
||||
- ' <content>## [Date] - [Decision Topic]'
|
||||
- ' **Context:** [What led to this decision]'
|
||||
- ' **Decision:** [What was decided]'
|
||||
- ' **Rationale:** [Why this decision was made]'
|
||||
- ' **Implementation:** [How it will be/was implemented]'
|
||||
- ' </content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' d. Update systemPatterns.md (if new patterns identified):'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/systemPatterns.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/systemPatterns.md</path>'
|
||||
- ' <content>[Add new patterns or update existing ones]</content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' e. Update productContext.md (if long-term context changes):'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/productContext.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/productContext.md</path>'
|
||||
- ' <content>[Update if project scope, goals, or major features changed]</content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' 4. Confirmation: After updates are complete, summarize changes made to each file.'
|
||||
251
.clinerules-code
Normal file
251
.clinerules-code
Normal file
@@ -0,0 +1,251 @@
|
||||
mode: code
|
||||
mode_switching:
|
||||
enabled: true
|
||||
preserve_context: true
|
||||
|
||||
real_time_updates:
|
||||
enabled: true
|
||||
update_triggers:
|
||||
project_related:
|
||||
- code_change
|
||||
- implementation_decision
|
||||
- bug_fix
|
||||
- feature_addition
|
||||
- refactoring
|
||||
system_related:
|
||||
- dependency_change
|
||||
- performance_optimization
|
||||
- security_fix
|
||||
- configuration_update
|
||||
documentation_related:
|
||||
- code_documentation
|
||||
- api_documentation
|
||||
- implementation_notes
|
||||
- usage_examples
|
||||
update_targets:
|
||||
high_priority:
|
||||
- activeContext.md
|
||||
- progress.md
|
||||
medium_priority:
|
||||
- decisionLog.md
|
||||
- productContext.md
|
||||
low_priority:
|
||||
- systemPatterns.md
|
||||
# Intent-based triggers
|
||||
intent_triggers:
|
||||
architect:
|
||||
- design
|
||||
- architect
|
||||
- structure
|
||||
- plan
|
||||
- organize
|
||||
ask:
|
||||
- explain
|
||||
- help
|
||||
- what
|
||||
- how
|
||||
- why
|
||||
- describe
|
||||
# Mode-specific triggers
|
||||
mode_triggers:
|
||||
architect:
|
||||
- condition: needs_design_review
|
||||
- condition: architecture_discussion
|
||||
ask:
|
||||
- condition: needs_explanation
|
||||
- condition: documentation_request
|
||||
|
||||
instructions:
|
||||
general:
|
||||
- "You are Roo's Code mode, an implementation-focused developer responsible for code creation, modification, and documentation. Your primary responsibilities are:"
|
||||
- " 1. Code implementation and modification"
|
||||
- " 2. Documentation updates during development"
|
||||
- " 3. Memory Bank maintenance during coding sessions"
|
||||
- " 4. Implementation of architectural decisions"
|
||||
- "You treat documentation as an integral part of the development process."
|
||||
- "Task Completion Behavior:"
|
||||
- " 1. After completing any task:"
|
||||
- " - Update relevant Memory Bank files in real-time"
|
||||
- " - If there are relevant implementation tasks, present them"
|
||||
- " - Otherwise ask: 'Is there anything else I can help you with?'"
|
||||
- " 2. NEVER use attempt_completion except:"
|
||||
- " - When explicitly requested by user"
|
||||
- " - When processing a UMB request with no additional instructions"
|
||||
- " 3. When providing multiple commands to be executed in the terminal,"
|
||||
- " present them all in a single line (e.g., 'command1 && command2') so users can copy and paste them directly."
|
||||
- "When a Memory Bank is found:"
|
||||
- " 1. Read ALL files in the memory-bank directory"
|
||||
- " 2. Check for core Memory Bank files:"
|
||||
- " - activeContext.md: Current session context"
|
||||
- " - productContext.md: Project overview"
|
||||
- " - progress.md: Progress tracking"
|
||||
- " - decisionLog.md: Decision logging"
|
||||
- " 3. If any core files are missing:"
|
||||
- " - Inform user about missing files"
|
||||
- " - Briefly explain their purposes"
|
||||
- " - Offer to create them"
|
||||
- " - Create files upon user approval"
|
||||
- " 4. Present available implementation tasks based on Memory Bank content"
|
||||
- " 5. Wait for user selection before proceeding"
|
||||
- " 6. Only use attempt_completion when explicitly requested by the user"
|
||||
- " or when processing a UMB request with no additional instructions"
|
||||
- " 7. For all other tasks, present results and ask if there is anything else you can help with"
|
||||
memory_bank:
|
||||
- "Status Prefix: Begin EVERY response with either '[MEMORY BANK: ACTIVE]' or '[MEMORY BANK: INACTIVE]'"
|
||||
- "Memory Bank Detection and Loading:"
|
||||
- " 1. On activation, scan workspace for memory-bank/ directories using:"
|
||||
- " <search_files>"
|
||||
- " <path>.</path>"
|
||||
- " <regex>memory-bank/</regex>"
|
||||
- " </search_files>"
|
||||
- " 2. If multiple memory-bank/ directories found:"
|
||||
- " - Present numbered list with full paths"
|
||||
- " - Ask: 'Which Memory Bank would you like to load? (Enter number)'"
|
||||
- " - Once selected, read ALL files in that memory-bank directory"
|
||||
- " 3. If one memory-bank/ found:"
|
||||
- " - Read ALL files in the memory-bank directory using list_dir and read_file"
|
||||
- " - Check for core Memory Bank files:"
|
||||
- " - activeContext.md"
|
||||
- " - productContext.md"
|
||||
- " - progress.md"
|
||||
- " - decisionLog.md"
|
||||
- " - If any core files are missing:"
|
||||
- " - List the missing core files"
|
||||
- " - Briefly explain their purposes"
|
||||
- " - Ask: 'Would you like me to create the missing core files? (yes/no)'"
|
||||
- " - Create files upon user approval"
|
||||
- " - Build comprehensive context from all available files"
|
||||
- " 4. If no memory-bank/ found:"
|
||||
- " - Switch to Architect mode for initialization"
|
||||
- "Memory Bank Initialization:"
|
||||
- " 1. When Memory Bank is found:"
|
||||
- " - Read ALL files in the memory-bank directory using list_dir and read_file"
|
||||
- " - Build comprehensive context from all available files"
|
||||
- " - Check for core Memory Bank files:"
|
||||
- " - activeContext.md: Current session context"
|
||||
- " - productContext.md: Project overview"
|
||||
- " - progress.md: Progress tracking"
|
||||
- " - decisionLog.md: Decision logging"
|
||||
- " - If any core files are missing:"
|
||||
- " - List the missing core files"
|
||||
- " - Explain their purposes"
|
||||
- " - Offer to create them"
|
||||
- " - Present available tasks based on ALL Memory Bank content"
|
||||
- " - Wait for user selection before proceeding"
|
||||
- "Memory Bank Maintenance:"
|
||||
- " 1. Real-time updates during development:"
|
||||
- " - activeContext.md: Immediately track tasks and progress"
|
||||
- " - progress.md: Record work as it's completed"
|
||||
- " - decisionLog.md: Log decisions as they're made"
|
||||
- " - productContext.md: Update implementation details"
|
||||
- " 2. Create new files when needed:"
|
||||
- " - Coordinate with Architect mode on file structure"
|
||||
- " - Document new files in existing Memory Bank files"
|
||||
- " 3. Monitor for update triggers:"
|
||||
- " - Code changes and implementations"
|
||||
- " - Bug fixes and optimizations"
|
||||
- " - Documentation updates"
|
||||
- " - System configuration changes"
|
||||
- "File Creation Authority:"
|
||||
- " - Can create and modify all Memory Bank files"
|
||||
- " - Focus on keeping documentation current with code"
|
||||
- " - Update existing files as code evolves"
|
||||
- "Mode Collaboration:"
|
||||
- " - Implement structures planned by Architect mode"
|
||||
- " - Keep documentation current for Ask mode"
|
||||
- " - Request architectural guidance when needed"
|
||||
umb:
|
||||
- '"Update Memory Bank" (UMB) in Code Mode:'
|
||||
- ' When the phrase "update memory bank" or "UMB" is used, Roo will:'
|
||||
- ' 1. Halt Current Task: Immediately stop any ongoing coding or documentation tasks.'
|
||||
- ' 2. Review Chat History:'
|
||||
- ' Option A - Direct Access:'
|
||||
- ' If chat history is directly accessible:'
|
||||
- ' - Review the entire chat session'
|
||||
- ' Option B - Export File:'
|
||||
- ' If chat history is not accessible:'
|
||||
- ' - Request user to click the "export" link in the pinned task box'
|
||||
- ' - Ask user to provide the path to the exported file'
|
||||
- ' - Read the exported file:'
|
||||
- ' <read_file>'
|
||||
- ' <path>[user-provided path to exported chat file]</path>'
|
||||
- ' </read_file>'
|
||||
- ' From either option, gather:'
|
||||
- ' - Changes made to the codebase'
|
||||
- ' - Decisions and their rationale'
|
||||
- ' - Current progress and status'
|
||||
- ' - New patterns or architectural insights'
|
||||
- ' - Open questions or issues'
|
||||
- ' 3. Update Memory Bank Files:'
|
||||
- ' a. Update activeContext.md:'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/activeContext.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/activeContext.md</path>'
|
||||
- ' <content>## Current Session Context'
|
||||
- ' [Date and time of update]'
|
||||
- ' '
|
||||
- ' ## Recent Changes'
|
||||
- ' [List of changes made in this session]'
|
||||
- ' '
|
||||
- ' ## Current Goals'
|
||||
- ' [Active and upcoming tasks]'
|
||||
- ' '
|
||||
- ' ## Open Questions'
|
||||
- ' [Any unresolved questions or issues]'
|
||||
- ' </content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' b. Update progress.md:'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/progress.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/progress.md</path>'
|
||||
- ' <content>## Work Done'
|
||||
- ' [New entries for completed work]'
|
||||
- ' '
|
||||
- ' ## Next Steps'
|
||||
- ' [Updated next steps based on current progress]'
|
||||
- ' </content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' c. Update decisionLog.md (if decisions were made):'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/decisionLog.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/decisionLog.md</path>'
|
||||
- ' <content>## [Date] - [Decision Topic]'
|
||||
- ' **Context:** [What led to this decision]'
|
||||
- ' **Decision:** [What was decided]'
|
||||
- ' **Rationale:** [Why this decision was made]'
|
||||
- ' **Implementation:** [How it will be/was implemented]'
|
||||
- ' </content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' d. Update systemPatterns.md (if new patterns identified):'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/systemPatterns.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/systemPatterns.md</path>'
|
||||
- ' <content>[Add new patterns or update existing ones]</content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' e. Update productContext.md (if long-term context changes):'
|
||||
- ' <read_file>'
|
||||
- ' <path>memory-bank/productContext.md</path>'
|
||||
- ' </read_file>'
|
||||
- ' Then update with:'
|
||||
- ' <apply_diff>'
|
||||
- ' <path>memory-bank/productContext.md</path>'
|
||||
- ' <content>[Update if project scope, goals, or major features changed]</content>'
|
||||
- ' <line_count>[computed from content]</line_count>'
|
||||
- ' </apply_diff>'
|
||||
- ' 4. Confirmation: After updates are complete, summarize changes made to each file.'
|
||||
22
.env.testing
Normal file
22
.env.testing
Normal file
@@ -0,0 +1,22 @@
|
||||
APP_NAME="ThrillWiki"
|
||||
APP_ENV=testing
|
||||
APP_KEY=base64:UzB6YEBskhM4O43pu4z4F8pscMD3A6NPWPpwZ+lkaMU=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=thrillwiki_test
|
||||
DB_USERNAME=talor
|
||||
DB_PASSWORD=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=array
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=array
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -21,3 +21,8 @@ yarn-error.log
|
||||
/.nova
|
||||
/.vscode
|
||||
/.zed
|
||||
.clinerules
|
||||
.clinerules
|
||||
.clinerules
|
||||
RooCode-Tips-Tricks-main
|
||||
.DS_Store
|
||||
|
||||
48
.roo/mcp.json
Normal file
48
.roo/mcp.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"git": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"mcp-server-git",
|
||||
"--repository",
|
||||
"/Volumes/macminissd/Projects/ThrillWiki/thrillwiki_laravel"
|
||||
]
|
||||
},
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
"/Volumes/macminissd/Projects/ThrillWiki/thrillwiki_laravel"
|
||||
]
|
||||
},
|
||||
"postgres": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-postgres",
|
||||
"postgresql://192.168.86.3:5432"
|
||||
]
|
||||
},
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"-e",
|
||||
"GITHUB_TOOLSETS",
|
||||
"-e",
|
||||
"GITHUB_READ_ONLY",
|
||||
"ghcr.io/github/github-mcp-server"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "",
|
||||
"GITHUB_TOOLSETS": "",
|
||||
"GITHUB_READ_ONLY": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
.roomodes
Normal file
14
.roomodes
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"customModes": [
|
||||
{
|
||||
"slug": "debug",
|
||||
"name": "Debug",
|
||||
"roleDefinition": "You are Roo, a meticulous problem-solver with surgical precision and expert level troubleshooting and debugging skills.\nYou begin by rigorously analyzing system behavior, environmental factors, and failure patterns through a read-only lens. Systematically isolate variables using incremental testing, controlled experiments, and targeted diagnostic tooling (logging, tracing, memory analysis, or simulated fault injection). Formulate hypotheses using first-principles reasoning, then validate through evidence-based verification cycles. Prioritize root cause identification over symptomatic fixes - trace error propagation through all abstraction layers while maintaining system integrity. When necessary, propose temporary instrumentation (non-breaking debug statements/metrics/assertions) for enhanced observability, explicitly marking these as provisional suggestions. Maintain strict separation between investigation (Debug Mode) and implementation (Code Mode): present actionable findings with risk assessments, then await explicit user confirmation before transitioning phases. Cross-validate all conclusions against documentation, historical patterns, and external knowledge bases. Implement tiered verification checkpoints: 1) Confirm understanding of observed behavior 2) Present forensic analysis with reproduction steps 3) Propose targeted fixes with rollback contingencies. Maintain atomic change proposals with clear success/failure criteria. Escalate complex scenarios through collaborative debugging sessions, offering multiple investigative pathways while preserving system state integrity.",
|
||||
"groups": [
|
||||
"read",
|
||||
"command"
|
||||
],
|
||||
"source": "project"
|
||||
}
|
||||
]
|
||||
}
|
||||
128
README.md
128
README.md
@@ -1,66 +1,106 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
# ThrillWiki - Laravel/Livewire Implementation
|
||||
|
||||
## About Laravel
|
||||
This is the Laravel/Livewire implementation of ThrillWiki, maintaining feature parity with the original Django project.
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
## Prerequisites
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
- PHP 8.1 or higher
|
||||
- PostgreSQL
|
||||
- Node.js and npm
|
||||
- Composer
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
## Setup Instructions
|
||||
|
||||
## Learning Laravel
|
||||
### 1. Environment Configuration
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
```bash
|
||||
# Copy the example environment file
|
||||
cp .env.example .env
|
||||
|
||||
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
|
||||
# Generate application key
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
Configure your `.env` file with the following essential settings:
|
||||
|
||||
## Laravel Sponsors
|
||||
```env
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=thrillwiki
|
||||
DB_USERNAME=your_username
|
||||
DB_PASSWORD=your_password
|
||||
```
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||
### 2. Database Setup
|
||||
|
||||
### Premium Partners
|
||||
1. Create PostgreSQL database:
|
||||
```sql
|
||||
CREATE DATABASE thrillwiki;
|
||||
```
|
||||
|
||||
- **[Vehikl](https://vehikl.com/)**
|
||||
- **[Tighten Co.](https://tighten.co)**
|
||||
- **[WebReinvent](https://webreinvent.com/)**
|
||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||
- **[64 Robots](https://64robots.com)**
|
||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
|
||||
- **[Cyber-Duck](https://cyber-duck.co.uk)**
|
||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||
- **[Jump24](https://jump24.co.uk)**
|
||||
- **[Redberry](https://redberry.international/laravel/)**
|
||||
- **[Active Logic](https://activelogic.com)**
|
||||
- **[byte5](https://byte5.de)**
|
||||
- **[OP.GG](https://op.gg)**
|
||||
2. Run migrations and seed the database:
|
||||
```bash
|
||||
php artisan migrate:fresh --seed
|
||||
```
|
||||
|
||||
## Contributing
|
||||
### 3. Install Dependencies
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
```bash
|
||||
# Install PHP dependencies
|
||||
composer install
|
||||
|
||||
## Code of Conduct
|
||||
# Install Node.js dependencies
|
||||
npm install
|
||||
```
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
### 4. Start Development Servers
|
||||
|
||||
## Security Vulnerabilities
|
||||
Run these commands in separate terminal windows:
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
```bash
|
||||
# Start Laravel development server
|
||||
php artisan serve
|
||||
|
||||
## License
|
||||
# Start Vite development server for asset compilation
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
For production:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 5. Clear Cache (If Needed)
|
||||
|
||||
```bash
|
||||
php artisan cache:clear && php artisan config:clear && php artisan route:clear && php artisan view:clear
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Assets Not Loading**
|
||||
- Ensure Vite is running (`npm run dev`)
|
||||
- For production, make sure assets are built (`npm run build`)
|
||||
|
||||
2. **Database Connection Issues**
|
||||
- Verify PostgreSQL is running
|
||||
- Check credentials in `.env` file
|
||||
- Ensure database exists and is accessible
|
||||
|
||||
3. **Migration Errors**
|
||||
- Check migration order in `database/migrations`
|
||||
- Ensure database is empty when running `migrate:fresh`
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
This implementation maintains strict feature parity with the original Django project. Key requirements:
|
||||
|
||||
- Feature-to-Feature matching with Django implementation
|
||||
- Identical API responses and data structures
|
||||
- Consistent UI/UX with original
|
||||
- Test coverage matching Django functionality
|
||||
|
||||
For detailed development guidelines, refer to the project documentation.
|
||||
|
||||
1163
app/Console/Commands/MakeThrillWikiCrud.php
Normal file
1163
app/Console/Commands/MakeThrillWikiCrud.php
Normal file
File diff suppressed because it is too large
Load Diff
392
app/Console/Commands/MakeThrillWikiLivewire.php
Normal file
392
app/Console/Commands/MakeThrillWikiLivewire.php
Normal file
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MakeThrillWikiLivewire extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'make:thrillwiki-livewire {name : The name of the component}
|
||||
{--reusable : Generate a reusable component with optimization traits}
|
||||
{--with-tests : Generate test files for the component}
|
||||
{--cached : Add caching optimization to the component}
|
||||
{--paginated : Add pagination support to the component}
|
||||
{--force : Overwrite existing files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Create a ThrillWiki-optimized Livewire component with built-in patterns and performance optimization';
|
||||
|
||||
protected Filesystem $files;
|
||||
|
||||
public function __construct(Filesystem $files)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->files = $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$name = $this->argument('name');
|
||||
$className = Str::studly($name);
|
||||
$kebabName = Str::kebab($name);
|
||||
|
||||
$this->info("🚀 Generating ThrillWiki Livewire Component: {$className}");
|
||||
|
||||
// Generate the component class
|
||||
$this->generateComponent($className, $kebabName);
|
||||
|
||||
// Generate the view file
|
||||
$this->generateView($className, $kebabName);
|
||||
|
||||
// Generate tests if requested
|
||||
if ($this->option('with-tests')) {
|
||||
$this->generateTest($className);
|
||||
}
|
||||
|
||||
$this->displaySummary($className, $kebabName);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function generateComponent(string $className, string $kebabName): void
|
||||
{
|
||||
$componentPath = app_path("Livewire/{$className}.php");
|
||||
|
||||
if ($this->files->exists($componentPath) && !$this->option('force')) {
|
||||
$this->error("Component {$className} already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$stub = $this->getComponentStub();
|
||||
$content = $this->replaceStubPlaceholders($stub, $className, $kebabName);
|
||||
|
||||
$this->files->ensureDirectoryExists(dirname($componentPath));
|
||||
$this->files->put($componentPath, $content);
|
||||
|
||||
$this->info("✅ Component created: app/Livewire/{$className}.php");
|
||||
}
|
||||
|
||||
protected function generateView(string $className, string $kebabName): void
|
||||
{
|
||||
$viewPath = resource_path("views/livewire/{$kebabName}.blade.php");
|
||||
|
||||
if ($this->files->exists($viewPath) && !$this->option('force')) {
|
||||
$this->error("View {$kebabName}.blade.php already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$stub = $this->getViewStub();
|
||||
$content = $this->replaceViewPlaceholders($stub, $className, $kebabName);
|
||||
|
||||
$this->files->ensureDirectoryExists(dirname($viewPath));
|
||||
$this->files->put($viewPath, $content);
|
||||
|
||||
$this->info("✅ View created: resources/views/livewire/{$kebabName}.blade.php");
|
||||
}
|
||||
|
||||
protected function generateTest(string $className): void
|
||||
{
|
||||
$testPath = base_path("tests/Feature/Livewire/{$className}Test.php");
|
||||
|
||||
if ($this->files->exists($testPath) && !$this->option('force')) {
|
||||
$this->error("Test {$className}Test already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$stub = $this->getTestStub();
|
||||
$content = $this->replaceTestPlaceholders($stub, $className);
|
||||
|
||||
$this->files->ensureDirectoryExists(dirname($testPath));
|
||||
$this->files->put($testPath, $content);
|
||||
|
||||
$this->info("✅ Test created: tests/Feature/Livewire/{$className}Test.php");
|
||||
}
|
||||
|
||||
protected function getComponentStub(): string
|
||||
{
|
||||
$traits = [];
|
||||
$imports = ['use Livewire\Component;'];
|
||||
$properties = [];
|
||||
$methods = [];
|
||||
|
||||
// Add pagination if requested
|
||||
if ($this->option('paginated')) {
|
||||
$imports[] = 'use Livewire\WithPagination;';
|
||||
$traits[] = 'WithPagination';
|
||||
$properties[] = ' protected $paginationTheme = \'tailwind\';';
|
||||
}
|
||||
|
||||
// Add caching optimization if requested
|
||||
if ($this->option('cached') || $this->option('reusable')) {
|
||||
$imports[] = 'use Illuminate\Support\Facades\Cache;';
|
||||
$methods[] = $this->getCachingMethods();
|
||||
}
|
||||
|
||||
// Build traits string
|
||||
$traitsString = empty($traits) ? '' : "\n use " . implode(', ', $traits) . ";\n";
|
||||
|
||||
// Build properties string
|
||||
$propertiesString = empty($properties) ? '' : "\n" . implode("\n", $properties) . "\n";
|
||||
|
||||
// Build methods string
|
||||
$methodsString = implode("\n\n", $methods);
|
||||
|
||||
return <<<PHP
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
{IMPORTS}
|
||||
|
||||
class {CLASS_NAME} extends Component
|
||||
{{TRAITS}{PROPERTIES}
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.{VIEW_NAME}');
|
||||
}{METHODS}
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
protected function getViewStub(): string
|
||||
{
|
||||
if ($this->option('reusable')) {
|
||||
return <<<BLADE
|
||||
{{-- ThrillWiki Reusable Component: {CLASS_NAME} --}}
|
||||
<div class="thrillwiki-component"
|
||||
x-data="{ loading: false }"
|
||||
wire:loading.class="opacity-50">
|
||||
|
||||
{{-- Component Header --}}
|
||||
<div class="component-header mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{CLASS_NAME}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{{-- Component Content --}}
|
||||
<div class="component-content">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
ThrillWiki component content goes here.
|
||||
</p>
|
||||
|
||||
{{-- Example interactive element --}}
|
||||
<button wire:click="\$refresh"
|
||||
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">
|
||||
Refresh Component
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Loading State --}}
|
||||
<div wire:loading wire:target="\$refresh"
|
||||
class="absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
|
||||
return <<<BLADE
|
||||
{{-- ThrillWiki Component: {CLASS_NAME} --}}
|
||||
<div class="thrillwiki-component">
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
{CLASS_NAME}
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
ThrillWiki component content goes here.
|
||||
</p>
|
||||
</div>
|
||||
BLADE;
|
||||
}
|
||||
|
||||
protected function getTestStub(): string
|
||||
{
|
||||
return <<<PHP
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Livewire;
|
||||
|
||||
use App\Livewire\{CLASS_NAME};
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class {CLASS_NAME}Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/** @test */
|
||||
public function component_can_render(): void
|
||||
{
|
||||
Livewire::test({CLASS_NAME}::class)
|
||||
->assertStatus(200)
|
||||
->assertSee('{CLASS_NAME}');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function component_can_mount_successfully(): void
|
||||
{
|
||||
Livewire::test({CLASS_NAME}::class)
|
||||
->assertStatus(200);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function component_follows_thrillwiki_patterns(): void
|
||||
{
|
||||
Livewire::test({CLASS_NAME}::class)
|
||||
->assertViewIs('livewire.{VIEW_NAME}');
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
protected function getCachingMethods(): string
|
||||
{
|
||||
return <<<PHP
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string \$suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . \$suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string \$key, \$callback, int \$ttl = 3600)
|
||||
{
|
||||
return Cache::remember(\$this->getCacheKey(\$key), \$ttl, \$callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string \$key = null): void
|
||||
{
|
||||
if (\$key) {
|
||||
Cache::forget(\$this->getCacheKey(\$key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
protected function replaceStubPlaceholders(string $stub, string $className, string $kebabName): string
|
||||
{
|
||||
$imports = ['use Livewire\Component;'];
|
||||
$traits = [];
|
||||
|
||||
if ($this->option('paginated')) {
|
||||
$imports[] = 'use Livewire\WithPagination;';
|
||||
$traits[] = 'WithPagination';
|
||||
}
|
||||
|
||||
if ($this->option('cached') || $this->option('reusable')) {
|
||||
$imports[] = 'use Illuminate\Support\Facades\Cache;';
|
||||
}
|
||||
|
||||
$traitsString = empty($traits) ? '' : "\n use " . implode(', ', $traits) . ";\n";
|
||||
$importsString = implode("\n", $imports);
|
||||
$methodsString = '';
|
||||
|
||||
if ($this->option('cached') || $this->option('reusable')) {
|
||||
$methodsString = "\n\n" . $this->getCachingMethods();
|
||||
}
|
||||
|
||||
return str_replace(
|
||||
['{IMPORTS}', '{CLASS_NAME}', '{VIEW_NAME}', '{TRAITS}', '{PROPERTIES}', '{METHODS}'],
|
||||
[$importsString, $className, $kebabName, $traitsString, '', $methodsString],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
protected function replaceViewPlaceholders(string $stub, string $className, string $kebabName): string
|
||||
{
|
||||
return str_replace(
|
||||
['{CLASS_NAME}', '{VIEW_NAME}'],
|
||||
[$className, $kebabName],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
protected function replaceTestPlaceholders(string $stub, string $className): string
|
||||
{
|
||||
return str_replace(
|
||||
['{CLASS_NAME}', '{VIEW_NAME}'],
|
||||
[$className, Str::kebab($className)],
|
||||
$stub
|
||||
);
|
||||
}
|
||||
|
||||
protected function displaySummary(string $className, string $kebabName): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info("🎉 ThrillWiki Livewire Component '{$className}' created successfully!");
|
||||
$this->newLine();
|
||||
|
||||
$this->comment("📁 Files Generated:");
|
||||
$this->line(" • app/Livewire/{$className}.php");
|
||||
$this->line(" • resources/views/livewire/{$kebabName}.blade.php");
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" • tests/Feature/Livewire/{$className}Test.php");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->comment("🚀 Features Added:");
|
||||
|
||||
if ($this->option('reusable')) {
|
||||
$this->line(" • Reusable component patterns with optimization traits");
|
||||
}
|
||||
|
||||
if ($this->option('cached')) {
|
||||
$this->line(" • Caching optimization methods");
|
||||
}
|
||||
|
||||
if ($this->option('paginated')) {
|
||||
$this->line(" • Pagination support with Tailwind theme");
|
||||
}
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" • Automated test suite with ThrillWiki patterns");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->comment("📝 Next Steps:");
|
||||
$this->line(" 1. Customize the component logic in app/Livewire/{$className}.php");
|
||||
$this->line(" 2. Update the view template in resources/views/livewire/{$kebabName}.blade.php");
|
||||
$this->line(" 3. Include the component in your templates with <livewire:{$kebabName} />");
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" 4. Run tests with: php artisan test --filter {$className}Test");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("✨ Happy coding with ThrillWiki acceleration patterns!");
|
||||
}
|
||||
}
|
||||
857
app/Console/Commands/MakeThrillWikiModel.php
Normal file
857
app/Console/Commands/MakeThrillWikiModel.php
Normal file
@@ -0,0 +1,857 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class MakeThrillWikiModel extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'make:thrillwiki-model {name : The name of the model}
|
||||
{--migration : Generate a migration file}
|
||||
{--factory : Generate a model factory}
|
||||
{--with-relationships : Include common ThrillWiki relationships}
|
||||
{--cached : Add caching traits and methods}
|
||||
{--api-resource : Generate API resource class}
|
||||
{--with-tests : Generate model tests}
|
||||
{--force : Overwrite existing files}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate a ThrillWiki model with optimized patterns, traits, and optional related files';
|
||||
|
||||
/**
|
||||
* ThrillWiki traits for different model types
|
||||
*/
|
||||
protected array $thrillWikiTraits = [
|
||||
'HasLocation' => 'App\\Traits\\HasLocation',
|
||||
'HasSlugHistory' => 'App\\Traits\\HasSlugHistory',
|
||||
'HasStatistics' => 'App\\Traits\\HasStatistics',
|
||||
'HasCaching' => 'App\\Traits\\HasCaching',
|
||||
'HasSoftDeletes' => 'Illuminate\\Database\\Eloquent\\SoftDeletes',
|
||||
'HasFactory' => 'Illuminate\\Database\\Eloquent\\Factories\\HasFactory',
|
||||
];
|
||||
|
||||
/**
|
||||
* Common ThrillWiki relationships by model type
|
||||
*/
|
||||
protected array $relationshipPatterns = [
|
||||
'Park' => [
|
||||
'areas' => 'hasMany:ParkArea',
|
||||
'rides' => 'hasManyThrough:Ride,ParkArea',
|
||||
'operator' => 'belongsTo:Operator',
|
||||
'photos' => 'morphMany:Photo',
|
||||
'reviews' => 'morphMany:Review',
|
||||
],
|
||||
'Ride' => [
|
||||
'park' => 'belongsTo:Park',
|
||||
'area' => 'belongsTo:ParkArea',
|
||||
'manufacturer' => 'belongsTo:Manufacturer',
|
||||
'designer' => 'belongsTo:Designer',
|
||||
'photos' => 'morphMany:Photo',
|
||||
'reviews' => 'morphMany:Review',
|
||||
],
|
||||
'Operator' => [
|
||||
'parks' => 'hasMany:Park',
|
||||
],
|
||||
'Manufacturer' => [
|
||||
'rides' => 'hasMany:Ride,manufacturer_id',
|
||||
],
|
||||
'Designer' => [
|
||||
'rides' => 'hasMany:Ride,designer_id',
|
||||
],
|
||||
'Review' => [
|
||||
'user' => 'belongsTo:User',
|
||||
'reviewable' => 'morphTo',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('🚀 Generating ThrillWiki Model for: ' . $this->argument('name'));
|
||||
|
||||
$name = $this->argument('name');
|
||||
$className = Str::studly($name);
|
||||
$tableName = Str::snake(Str::plural($name));
|
||||
|
||||
// Generate model
|
||||
$this->generateModel($className);
|
||||
|
||||
// Generate optional files
|
||||
if ($this->option('migration')) {
|
||||
$this->generateMigration($className, $tableName);
|
||||
}
|
||||
|
||||
if ($this->option('factory')) {
|
||||
$this->generateFactory($className);
|
||||
}
|
||||
|
||||
if ($this->option('api-resource')) {
|
||||
$this->generateApiResource($className);
|
||||
}
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->generateTests($className);
|
||||
}
|
||||
|
||||
$this->displaySummary($className);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the model file
|
||||
*/
|
||||
protected function generateModel(string $className): void
|
||||
{
|
||||
$modelPath = app_path("Models/{$className}.php");
|
||||
|
||||
if (File::exists($modelPath) && !$this->option('force')) {
|
||||
$this->error("Model {$className} already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$modelContent = $this->buildModelContent($className);
|
||||
|
||||
$this->ensureDirectoryExists(dirname($modelPath));
|
||||
File::put($modelPath, $modelContent);
|
||||
|
||||
$this->line("✅ Model created: app/Models/{$className}.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the model content with ThrillWiki patterns
|
||||
*/
|
||||
protected function buildModelContent(string $className): string
|
||||
{
|
||||
$tableName = Str::snake(Str::plural($className));
|
||||
$traits = $this->getTraitsForModel($className);
|
||||
$relationships = $this->getRelationshipsForModel($className);
|
||||
$cachingMethods = $this->option('cached') ? $this->getCachingMethods($className) : '';
|
||||
|
||||
$stub = <<<'PHP'
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
{TRAIT_IMPORTS}
|
||||
|
||||
/**
|
||||
* {CLASS_NAME} Model
|
||||
*
|
||||
* Generated by ThrillWiki Model Generator
|
||||
* Includes ThrillWiki optimization patterns and performance enhancements
|
||||
*/
|
||||
class {CLASS_NAME} extends Model
|
||||
{
|
||||
{TRAITS}
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = '{TABLE_NAME}';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'is_active',
|
||||
// Add more fillable attributes as needed
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
// Add more casts as needed
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for arrays.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
// Add hidden attributes if needed
|
||||
];
|
||||
|
||||
// Query Scopes
|
||||
|
||||
/**
|
||||
* Scope a query to only include active records.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope for optimized queries with common relationships.
|
||||
*/
|
||||
public function scopeOptimized($query)
|
||||
{
|
||||
return $query->with($this->getOptimizedRelations());
|
||||
}
|
||||
|
||||
// ThrillWiki Methods
|
||||
|
||||
/**
|
||||
* Get optimized relations for this model.
|
||||
*/
|
||||
public function getOptimizedRelations(): array
|
||||
{
|
||||
return [
|
||||
// Define common relationships to eager load
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this model instance.
|
||||
*/
|
||||
public function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
$key = strtolower(class_basename($this)) . '.' . $this->id;
|
||||
return $suffix ? $key . '.' . $suffix : $key;
|
||||
}
|
||||
|
||||
{RELATIONSHIPS}
|
||||
|
||||
{CACHING_METHODS}
|
||||
}
|
||||
PHP;
|
||||
|
||||
return str_replace([
|
||||
'{CLASS_NAME}',
|
||||
'{TABLE_NAME}',
|
||||
'{TRAIT_IMPORTS}',
|
||||
'{TRAITS}',
|
||||
'{RELATIONSHIPS}',
|
||||
'{CACHING_METHODS}',
|
||||
], [
|
||||
$className,
|
||||
$tableName,
|
||||
$this->buildTraitImports($traits),
|
||||
$this->buildTraitUses($traits),
|
||||
$relationships,
|
||||
$cachingMethods,
|
||||
], $stub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get traits for the model based on options and model type
|
||||
*/
|
||||
protected function getTraitsForModel(string $className): array
|
||||
{
|
||||
$traits = ['HasFactory']; // Always include HasFactory
|
||||
|
||||
// Add SoftDeletes for most models
|
||||
$traits[] = 'HasSoftDeletes';
|
||||
|
||||
// Add caching if requested
|
||||
if ($this->option('cached')) {
|
||||
$traits[] = 'HasCaching';
|
||||
}
|
||||
|
||||
// Add location trait for location-based models
|
||||
if (in_array($className, ['Park', 'Company', 'ParkArea'])) {
|
||||
$traits[] = 'HasLocation';
|
||||
}
|
||||
|
||||
// Add slug history for main entities
|
||||
if (in_array($className, ['Park', 'Ride', 'Company', 'Designer'])) {
|
||||
$traits[] = 'HasSlugHistory';
|
||||
}
|
||||
|
||||
// Add statistics for countable entities
|
||||
if (in_array($className, ['Park', 'Ride', 'User'])) {
|
||||
$traits[] = 'HasStatistics';
|
||||
}
|
||||
|
||||
return $traits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build trait import statements
|
||||
*/
|
||||
protected function buildTraitImports(array $traits): string
|
||||
{
|
||||
$imports = [];
|
||||
foreach ($traits as $trait) {
|
||||
if (isset($this->thrillWikiTraits[$trait])) {
|
||||
$imports[] = "use {$this->thrillWikiTraits[$trait]};";
|
||||
}
|
||||
}
|
||||
return implode("\n", $imports);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build trait use statements
|
||||
*/
|
||||
protected function buildTraitUses(array $traits): string
|
||||
{
|
||||
$uses = array_map(function($trait) {
|
||||
return " use {$trait};";
|
||||
}, $traits);
|
||||
|
||||
return implode("\n", $uses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relationships for the model
|
||||
*/
|
||||
protected function getRelationshipsForModel(string $className): string
|
||||
{
|
||||
if (!$this->option('with-relationships')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!isset($this->relationshipPatterns[$className])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$relationships = [];
|
||||
foreach ($this->relationshipPatterns[$className] as $method => $definition) {
|
||||
$relationships[] = $this->buildRelationshipMethod($method, $definition);
|
||||
}
|
||||
|
||||
return "\n // Relationships\n\n" . implode("\n\n", $relationships);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a relationship method
|
||||
*/
|
||||
protected function buildRelationshipMethod(string $method, string $definition): string
|
||||
{
|
||||
[$type, $model, $foreignKey] = array_pad(explode(',', str_replace(':', ',', $definition)), 3, null);
|
||||
|
||||
$methodBody = match($type) {
|
||||
'hasMany' => $foreignKey ?
|
||||
"return \$this->hasMany({$model}::class, '{$foreignKey}');" :
|
||||
"return \$this->hasMany({$model}::class);",
|
||||
'belongsTo' => $foreignKey ?
|
||||
"return \$this->belongsTo({$model}::class, '{$foreignKey}');" :
|
||||
"return \$this->belongsTo({$model}::class);",
|
||||
'hasManyThrough' => "return \$this->hasManyThrough({$model}::class, {$foreignKey}::class);",
|
||||
'morphMany' => "return \$this->morphMany({$model}::class, 'morphable');",
|
||||
'morphTo' => "return \$this->morphTo();",
|
||||
default => "return \$this->{$type}({$model}::class);"
|
||||
};
|
||||
|
||||
return " /**\n * Get the {$method} relationship.\n */\n public function {$method}()\n {\n {$methodBody}\n }";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get caching methods for the model
|
||||
*/
|
||||
protected function getCachingMethods(string $className): string
|
||||
{
|
||||
return <<<'PHP'
|
||||
|
||||
// Caching Methods
|
||||
|
||||
/**
|
||||
* Remember a value in cache with model-specific key.
|
||||
*/
|
||||
public function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return cache()->remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for this model.
|
||||
*/
|
||||
public function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
cache()->forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache keys for this model
|
||||
cache()->forget($this->getCacheKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot method to handle cache invalidation.
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::saved(function ($model) {
|
||||
$model->invalidateCache();
|
||||
});
|
||||
|
||||
static::deleted(function ($model) {
|
||||
$model->invalidateCache();
|
||||
});
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate migration file
|
||||
*/
|
||||
protected function generateMigration(string $className, string $tableName): void
|
||||
{
|
||||
$migrationName = 'create_' . $tableName . '_table';
|
||||
$timestamp = date('Y_m_d_His');
|
||||
$migrationFile = database_path("migrations/{$timestamp}_{$migrationName}.php");
|
||||
|
||||
$migrationContent = $this->buildMigrationContent($className, $tableName, $migrationName);
|
||||
|
||||
File::put($migrationFile, $migrationContent);
|
||||
$this->line("✅ Migration created: database/migrations/{$timestamp}_{$migrationName}.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build migration content
|
||||
*/
|
||||
protected function buildMigrationContent(string $className, string $tableName, string $migrationName): string
|
||||
{
|
||||
$migrationClass = Str::studly($migrationName);
|
||||
|
||||
$stub = <<<'PHP'
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('{TABLE_NAME}', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
|
||||
// Add common ThrillWiki fields
|
||||
$table->string('slug')->unique();
|
||||
|
||||
// Add indexes for performance
|
||||
$table->index(['is_active']);
|
||||
$table->index(['name']);
|
||||
$table->index(['slug']);
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('{TABLE_NAME}');
|
||||
}
|
||||
};
|
||||
PHP;
|
||||
|
||||
return str_replace('{TABLE_NAME}', $tableName, $stub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate factory file
|
||||
*/
|
||||
protected function generateFactory(string $className): void
|
||||
{
|
||||
$factoryPath = database_path("factories/{$className}Factory.php");
|
||||
|
||||
if (File::exists($factoryPath) && !$this->option('force')) {
|
||||
$this->error("Factory {$className}Factory already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$factoryContent = $this->buildFactoryContent($className);
|
||||
|
||||
$this->ensureDirectoryExists(dirname($factoryPath));
|
||||
File::put($factoryPath, $factoryContent);
|
||||
|
||||
$this->line("✅ Factory created: database/factories/{$className}Factory.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build factory content
|
||||
*/
|
||||
protected function buildFactoryContent(string $className): string
|
||||
{
|
||||
$stub = <<<'PHP'
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\{CLASS_NAME};
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\{CLASS_NAME}>
|
||||
*/
|
||||
class {CLASS_NAME}Factory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = {CLASS_NAME}::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$name = $this->faker->unique()->words(2, true);
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'slug' => Str::slug($name),
|
||||
'description' => $this->faker->paragraphs(2, true),
|
||||
'is_active' => $this->faker->boolean(90), // 90% chance of being active
|
||||
'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
|
||||
'updated_at' => function (array $attributes) {
|
||||
return $this->faker->dateTimeBetween($attributes['created_at'], 'now');
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model is active.
|
||||
*/
|
||||
public function active(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model is inactive.
|
||||
*/
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
return str_replace('{CLASS_NAME}', $className, $stub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate API resource
|
||||
*/
|
||||
protected function generateApiResource(string $className): void
|
||||
{
|
||||
$resourcePath = app_path("Http/Resources/{$className}Resource.php");
|
||||
|
||||
if (File::exists($resourcePath) && !$this->option('force')) {
|
||||
$this->error("Resource {$className}Resource already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$resourceContent = $this->buildApiResourceContent($className);
|
||||
|
||||
$this->ensureDirectoryExists(dirname($resourcePath));
|
||||
File::put($resourcePath, $resourceContent);
|
||||
|
||||
$this->line("✅ API Resource created: app/Http/Resources/{$className}Resource.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build API resource content
|
||||
*/
|
||||
protected function buildApiResourceContent(string $className): string
|
||||
{
|
||||
$stub = <<<'PHP'
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* {CLASS_NAME} API Resource
|
||||
*
|
||||
* Transforms {CLASS_NAME} model data for API responses
|
||||
* Includes ThrillWiki optimization patterns
|
||||
*/
|
||||
class {CLASS_NAME}Resource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'description' => $this->description,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
|
||||
// Include relationships when loaded
|
||||
$this->mergeWhen($this->relationLoaded('relationships'), [
|
||||
// Add relationship data here
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional data that should be returned with the resource array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function with(Request $request): array
|
||||
{
|
||||
return [
|
||||
'meta' => [
|
||||
'model' => '{CLASS_NAME}',
|
||||
'generated_at' => now()->toISOString(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
return str_replace('{CLASS_NAME}', $className, $stub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate test files
|
||||
*/
|
||||
protected function generateTests(string $className): void
|
||||
{
|
||||
$testPath = base_path("tests/Feature/{$className}Test.php");
|
||||
|
||||
if (File::exists($testPath) && !$this->option('force')) {
|
||||
$this->error("Test {$className}Test already exists! Use --force to overwrite.");
|
||||
return;
|
||||
}
|
||||
|
||||
$testContent = $this->buildTestContent($className);
|
||||
|
||||
$this->ensureDirectoryExists(dirname($testPath));
|
||||
File::put($testPath, $testContent);
|
||||
|
||||
$this->line("✅ Test created: tests/Feature/{$className}Test.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build test content
|
||||
*/
|
||||
protected function buildTestContent(string $className): string
|
||||
{
|
||||
$stub = <<<'PHP'
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\{CLASS_NAME};
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* {CLASS_NAME} Model Feature Tests
|
||||
*
|
||||
* Tests for ThrillWiki {CLASS_NAME} model functionality
|
||||
*/
|
||||
class {CLASS_NAME}Test extends TestCase
|
||||
{
|
||||
use RefreshDatabase, WithFaker;
|
||||
|
||||
/**
|
||||
* Test model creation.
|
||||
*/
|
||||
public function test_can_create_{LOWER_CLASS_NAME}(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
|
||||
|
||||
$this->assertDatabaseHas('{TABLE_NAME}', [
|
||||
'id' => ${LOWER_CLASS_NAME}->id,
|
||||
'name' => ${LOWER_CLASS_NAME}->name,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test model factory.
|
||||
*/
|
||||
public function test_{LOWER_CLASS_NAME}_factory_works(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
|
||||
|
||||
$this->assertInstanceOf({CLASS_NAME}::class, ${LOWER_CLASS_NAME});
|
||||
$this->assertNotEmpty(${LOWER_CLASS_NAME}->name);
|
||||
$this->assertIsBool(${LOWER_CLASS_NAME}->is_active);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test active scope.
|
||||
*/
|
||||
public function test_active_scope_filters_correctly(): void
|
||||
{
|
||||
{CLASS_NAME}::factory()->active()->create();
|
||||
{CLASS_NAME}::factory()->inactive()->create();
|
||||
|
||||
$activeCount = {CLASS_NAME}::active()->count();
|
||||
$totalCount = {CLASS_NAME}::count();
|
||||
|
||||
$this->assertEquals(1, $activeCount);
|
||||
$this->assertEquals(2, $totalCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cache key generation.
|
||||
*/
|
||||
public function test_cache_key_generation(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
|
||||
|
||||
$cacheKey = ${LOWER_CLASS_NAME}->getCacheKey();
|
||||
$expectedKey = strtolower('{LOWER_CLASS_NAME}') . '.' . ${LOWER_CLASS_NAME}->id;
|
||||
|
||||
$this->assertEquals($expectedKey, $cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cache key with suffix.
|
||||
*/
|
||||
public function test_cache_key_with_suffix(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
|
||||
|
||||
$cacheKey = ${LOWER_CLASS_NAME}->getCacheKey('details');
|
||||
$expectedKey = strtolower('{LOWER_CLASS_NAME}') . '.' . ${LOWER_CLASS_NAME}->id . '.details';
|
||||
|
||||
$this->assertEquals($expectedKey, $cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test soft deletes.
|
||||
*/
|
||||
public function test_soft_deletes_work(): void
|
||||
{
|
||||
${LOWER_CLASS_NAME} = {CLASS_NAME}::factory()->create();
|
||||
${LOWER_CLASS_NAME}->delete();
|
||||
|
||||
$this->assertSoftDeleted(${LOWER_CLASS_NAME});
|
||||
|
||||
// Test that it's excluded from normal queries
|
||||
$this->assertEquals(0, {CLASS_NAME}::count());
|
||||
|
||||
// Test that it's included in withTrashed queries
|
||||
$this->assertEquals(1, {CLASS_NAME}::withTrashed()->count());
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
return str_replace([
|
||||
'{CLASS_NAME}',
|
||||
'{LOWER_CLASS_NAME}',
|
||||
'{TABLE_NAME}',
|
||||
], [
|
||||
$className,
|
||||
strtolower($className),
|
||||
Str::snake(Str::plural($className)),
|
||||
], $stub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure directory exists
|
||||
*/
|
||||
protected function ensureDirectoryExists(string $directory): void
|
||||
{
|
||||
if (!File::isDirectory($directory)) {
|
||||
File::makeDirectory($directory, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display summary of generated files
|
||||
*/
|
||||
protected function displaySummary(string $className): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->info("🎉 ThrillWiki Model '{$className}' created successfully!");
|
||||
$this->newLine();
|
||||
|
||||
$this->line("📁 Files Generated:");
|
||||
$this->line(" • app/Models/{$className}.php");
|
||||
|
||||
if ($this->option('migration')) {
|
||||
$this->line(" • database/migrations/[timestamp]_create_" . Str::snake(Str::plural($className)) . "_table.php");
|
||||
}
|
||||
|
||||
if ($this->option('factory')) {
|
||||
$this->line(" • database/factories/{$className}Factory.php");
|
||||
}
|
||||
|
||||
if ($this->option('api-resource')) {
|
||||
$this->line(" • app/Http/Resources/{$className}Resource.php");
|
||||
}
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" • tests/Feature/{$className}Test.php");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->line("🚀 Next Steps:");
|
||||
|
||||
if ($this->option('migration')) {
|
||||
$this->line(" 1. Run migration: php artisan migrate");
|
||||
}
|
||||
|
||||
if ($this->option('with-tests')) {
|
||||
$this->line(" 2. Run tests: php artisan test --filter {$className}Test");
|
||||
}
|
||||
|
||||
$this->line(" 3. Customize model attributes and relationships");
|
||||
$this->line(" 4. Update migration with specific fields");
|
||||
|
||||
if ($this->option('factory')) {
|
||||
$this->line(" 5. Customize factory with realistic data");
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
27
app/Console/Kernel.php
Normal file
27
app/Console/Kernel.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*/
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
38
app/Enums/LaunchType.php
Normal file
38
app/Enums/LaunchType.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum LaunchType: string
|
||||
{
|
||||
case CHAIN = 'CHAIN';
|
||||
case LSM = 'LSM';
|
||||
case HYDRAULIC = 'HYDRAULIC';
|
||||
case GRAVITY = 'GRAVITY';
|
||||
case OTHER = 'OTHER';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::CHAIN => 'Chain Lift',
|
||||
self::LSM => 'LSM Launch',
|
||||
self::HYDRAULIC => 'Hydraulic Launch',
|
||||
self::GRAVITY => 'Gravity',
|
||||
self::OTHER => 'Other',
|
||||
};
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->label(), self::cases());
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_combine(self::values(), self::labels());
|
||||
}
|
||||
}
|
||||
28
app/Enums/ReviewStatus.php
Normal file
28
app/Enums/ReviewStatus.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ReviewStatus: string
|
||||
{
|
||||
case PENDING = 'pending';
|
||||
case APPROVED = 'approved';
|
||||
case REJECTED = 'rejected';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::PENDING => 'Pending',
|
||||
self::APPROVED => 'Approved',
|
||||
self::REJECTED => 'Rejected',
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::PENDING => 'yellow',
|
||||
self::APPROVED => 'green',
|
||||
self::REJECTED => 'red',
|
||||
};
|
||||
}
|
||||
}
|
||||
42
app/Enums/RideCategory.php
Normal file
42
app/Enums/RideCategory.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RideCategory: string
|
||||
{
|
||||
case SELECT = '';
|
||||
case ROLLER_COASTER = 'RC';
|
||||
case DARK_RIDE = 'DR';
|
||||
case FLAT_RIDE = 'FR';
|
||||
case WATER_RIDE = 'WR';
|
||||
case TRANSPORT = 'TR';
|
||||
case OTHER = 'OT';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::SELECT => 'Select ride type',
|
||||
self::ROLLER_COASTER => 'Roller Coaster',
|
||||
self::DARK_RIDE => 'Dark Ride',
|
||||
self::FLAT_RIDE => 'Flat Ride',
|
||||
self::WATER_RIDE => 'Water Ride',
|
||||
self::TRANSPORT => 'Transport',
|
||||
self::OTHER => 'Other',
|
||||
};
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->label(), self::cases());
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_combine(self::values(), self::labels());
|
||||
}
|
||||
}
|
||||
71
app/Enums/RideStatus.php
Normal file
71
app/Enums/RideStatus.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RideStatus: string
|
||||
{
|
||||
case SELECT = '';
|
||||
case OPERATING = 'OPERATING';
|
||||
case CLOSED_TEMP = 'CLOSED_TEMP';
|
||||
case SBNO = 'SBNO';
|
||||
case CLOSING = 'CLOSING';
|
||||
case CLOSED_PERM = 'CLOSED_PERM';
|
||||
case UNDER_CONSTRUCTION = 'UNDER_CONSTRUCTION';
|
||||
case DEMOLISHED = 'DEMOLISHED';
|
||||
case RELOCATED = 'RELOCATED';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::SELECT => 'Select status',
|
||||
self::OPERATING => 'Operating',
|
||||
self::CLOSED_TEMP => 'Temporarily Closed',
|
||||
self::SBNO => 'Standing But Not Operating',
|
||||
self::CLOSING => 'Closing',
|
||||
self::CLOSED_PERM => 'Permanently Closed',
|
||||
self::UNDER_CONSTRUCTION => 'Under Construction',
|
||||
self::DEMOLISHED => 'Demolished',
|
||||
self::RELOCATED => 'Relocated',
|
||||
};
|
||||
}
|
||||
|
||||
public function isPostClosingStatus(): bool
|
||||
{
|
||||
return in_array($this, [
|
||||
self::SBNO,
|
||||
self::CLOSED_PERM,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function postClosingStatuses(): array
|
||||
{
|
||||
return [
|
||||
self::SBNO,
|
||||
self::CLOSED_PERM,
|
||||
];
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->label(), self::cases());
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_combine(self::values(), self::labels());
|
||||
}
|
||||
|
||||
public static function postClosingOptions(): array
|
||||
{
|
||||
$statuses = array_filter(self::cases(), fn($case) => $case->isPostClosingStatus());
|
||||
return array_combine(
|
||||
array_column($statuses, 'value'),
|
||||
array_map(fn($case) => $case->label(), $statuses)
|
||||
);
|
||||
}
|
||||
}
|
||||
50
app/Enums/RollerCoasterType.php
Normal file
50
app/Enums/RollerCoasterType.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RollerCoasterType: string
|
||||
{
|
||||
case SITDOWN = 'SITDOWN';
|
||||
case INVERTED = 'INVERTED';
|
||||
case FLYING = 'FLYING';
|
||||
case STANDUP = 'STANDUP';
|
||||
case WING = 'WING';
|
||||
case DIVE = 'DIVE';
|
||||
case FAMILY = 'FAMILY';
|
||||
case WILD_MOUSE = 'WILD_MOUSE';
|
||||
case SPINNING = 'SPINNING';
|
||||
case FOURTH_DIMENSION = 'FOURTH_DIMENSION';
|
||||
case OTHER = 'OTHER';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::SITDOWN => 'Sit Down',
|
||||
self::INVERTED => 'Inverted',
|
||||
self::FLYING => 'Flying',
|
||||
self::STANDUP => 'Stand Up',
|
||||
self::WING => 'Wing',
|
||||
self::DIVE => 'Dive',
|
||||
self::FAMILY => 'Family',
|
||||
self::WILD_MOUSE => 'Wild Mouse',
|
||||
self::SPINNING => 'Spinning',
|
||||
self::FOURTH_DIMENSION => '4th Dimension',
|
||||
self::OTHER => 'Other',
|
||||
};
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->label(), self::cases());
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_combine(self::values(), self::labels());
|
||||
}
|
||||
}
|
||||
34
app/Enums/TrackMaterial.php
Normal file
34
app/Enums/TrackMaterial.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum TrackMaterial: string
|
||||
{
|
||||
case STEEL = 'STEEL';
|
||||
case WOOD = 'WOOD';
|
||||
case HYBRID = 'HYBRID';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::STEEL => 'Steel',
|
||||
self::WOOD => 'Wood',
|
||||
self::HYBRID => 'Hybrid',
|
||||
};
|
||||
}
|
||||
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
public static function labels(): array
|
||||
{
|
||||
return array_map(fn($case) => $case->label(), self::cases());
|
||||
}
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return array_combine(self::values(), self::labels());
|
||||
}
|
||||
}
|
||||
10
app/Exceptions/GeocodingException.php
Normal file
10
app/Exceptions/GeocodingException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class GeocodingException extends Exception
|
||||
{
|
||||
// Custom exception for geocoding-related errors
|
||||
}
|
||||
30
app/Exceptions/Handler.php
Normal file
30
app/Exceptions/Handler.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* The list of the inputs that are never flashed to the session on validation exceptions.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
//
|
||||
});
|
||||
}
|
||||
}
|
||||
10
app/Exceptions/ValidationException.php
Normal file
10
app/Exceptions/ValidationException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ValidationException extends Exception
|
||||
{
|
||||
// Custom exception for validation-related errors
|
||||
}
|
||||
125
app/Filament/Resources/DesignerResource.php
Normal file
125
app/Filament/Resources/DesignerResource.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\DesignerResource\Pages;
|
||||
use App\Filament\Resources\DesignerResource\RelationManagers;
|
||||
use App\Models\Designer;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DesignerResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Designer::class;
|
||||
protected static ?string $navigationIcon = 'heroicon-o-building-office';
|
||||
protected static ?string $navigationGroup = 'Company Management';
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make('Basic Information')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, Forms\Set $set) =>
|
||||
$set('slug', str($state)->slug())),
|
||||
Forms\Components\TextInput::make('slug')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true),
|
||||
Forms\Components\TextInput::make('headquarters')
|
||||
->maxLength(255),
|
||||
Forms\Components\DatePicker::make('founded_date')
|
||||
->label('Founded Date')
|
||||
->format('Y-m-d'),
|
||||
])->columns(2),
|
||||
|
||||
Forms\Components\Section::make('Additional Details')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('website')
|
||||
->url()
|
||||
->prefix('https://')
|
||||
->maxLength(255),
|
||||
Forms\Components\RichEditor::make('description')
|
||||
->columnSpanFull()
|
||||
->toolbarButtons([
|
||||
'bold',
|
||||
'italic',
|
||||
'link',
|
||||
'bulletList',
|
||||
'orderedList',
|
||||
'h2',
|
||||
'h3',
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('headquarters')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('founded_date')
|
||||
->date()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('rides_count')
|
||||
->counts('rides')
|
||||
->label('Rides')
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('website')
|
||||
->searchable()
|
||||
->url(fn ($state) => str($state)->start('https://')),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\Filter::make('has_rides')
|
||||
->query(fn (Builder $query) => $query->has('rides'))
|
||||
->label('Has Rides'),
|
||||
Tables\Filters\Filter::make('no_rides')
|
||||
->query(fn (Builder $query) => $query->doesntHave('rides'))
|
||||
->label('No Rides'),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\ViewAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
RelationManagers\RidesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListDesigners::route('/'),
|
||||
'create' => Pages\CreateDesigner::route('/create'),
|
||||
'edit' => Pages\EditDesigner::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DesignerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DesignerResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateDesigner extends CreateRecord
|
||||
{
|
||||
protected static string $resource = DesignerResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DesignerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DesignerResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditDesigner extends EditRecord
|
||||
{
|
||||
protected static string $resource = DesignerResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DesignerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DesignerResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListDesigners extends ListRecords
|
||||
{
|
||||
protected static string $resource = DesignerResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DesignerResource\RelationManagers;
|
||||
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class RidesRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'rides';
|
||||
protected static ?string $title = 'Rides';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('manufacturer_name')
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('model_name')
|
||||
->maxLength(255),
|
||||
Forms\Components\DatePicker::make('opened_date')
|
||||
->label('Opening Date'),
|
||||
Forms\Components\DatePicker::make('closed_date')
|
||||
->label('Closing Date')
|
||||
->after('opened_date'),
|
||||
Forms\Components\Textarea::make('description')
|
||||
->columnSpanFull(),
|
||||
Forms\Components\Toggle::make('is_active')
|
||||
->label('Active')
|
||||
->default(true),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('manufacturer_name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('opened_date')
|
||||
->date()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('closed_date')
|
||||
->date()
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('is_active')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\TrashedFilter::make(),
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make(),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
Tables\Actions\DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
162
app/Filament/Resources/RideResource.php
Normal file
162
app/Filament/Resources/RideResource.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\RideResource\Pages;
|
||||
use App\Filament\Resources\RideResource\RelationManagers;
|
||||
use App\Models\Ride;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class RideResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Ride::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('slug')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\Textarea::make('description')
|
||||
->required()
|
||||
->columnSpanFull(),
|
||||
Forms\Components\Select::make('park_id')
|
||||
->relationship('park', 'name')
|
||||
->required(),
|
||||
Forms\Components\Select::make('park_area_id')
|
||||
->relationship('parkArea', 'name'),
|
||||
Forms\Components\Select::make('manufacturer_id')
|
||||
->relationship('manufacturer', 'name'),
|
||||
Forms\Components\Select::make('designer_id')
|
||||
->relationship('designer', 'name'),
|
||||
Forms\Components\Select::make('ride_model_id')
|
||||
->relationship('rideModel', 'name'),
|
||||
Forms\Components\TextInput::make('category')
|
||||
->required()
|
||||
->maxLength(2)
|
||||
->default(''),
|
||||
Forms\Components\TextInput::make('status')
|
||||
->required()
|
||||
->maxLength(20)
|
||||
->default('OPERATING'),
|
||||
Forms\Components\TextInput::make('post_closing_status')
|
||||
->maxLength(20),
|
||||
Forms\Components\DatePicker::make('opening_date'),
|
||||
Forms\Components\DatePicker::make('closing_date'),
|
||||
Forms\Components\DatePicker::make('status_since'),
|
||||
Forms\Components\TextInput::make('min_height_in')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('max_height_in')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('capacity_per_hour')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('ride_duration_seconds')
|
||||
->numeric(),
|
||||
Forms\Components\TextInput::make('average_rating')
|
||||
->numeric(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('slug')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('park.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('parkArea.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('manufacturer.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('designer.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('rideModel.name')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('category')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('status')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('post_closing_status')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('opening_date')
|
||||
->date()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('closing_date')
|
||||
->date()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('status_since')
|
||||
->date()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('min_height_in')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('max_height_in')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('capacity_per_hour')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('ride_duration_seconds')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('average_rating')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
Tables\Columns\TextColumn::make('updated_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListRides::route('/'),
|
||||
'create' => Pages\CreateRide::route('/create'),
|
||||
'edit' => Pages\EditRide::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
12
app/Filament/Resources/RideResource/Pages/CreateRide.php
Normal file
12
app/Filament/Resources/RideResource/Pages/CreateRide.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RideResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateRide extends CreateRecord
|
||||
{
|
||||
protected static string $resource = RideResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/RideResource/Pages/EditRide.php
Normal file
19
app/Filament/Resources/RideResource/Pages/EditRide.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RideResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditRide extends EditRecord
|
||||
{
|
||||
protected static string $resource = RideResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/RideResource/Pages/ListRides.php
Normal file
19
app/Filament/Resources/RideResource/Pages/ListRides.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RideResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RideResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListRides extends ListRecords
|
||||
{
|
||||
protected static string $resource = RideResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
95
app/Http/Controllers/Api/OperatorController.php
Normal file
95
app/Http/Controllers/Api/OperatorController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Operator;
|
||||
use App\Http\Requests\OperatorRequest;
|
||||
use App\Http\Resources\OperatorResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class OperatorController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Operator::query();
|
||||
|
||||
// Search functionality
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->get('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'ILIKE', "%{$search}%")
|
||||
->orWhere('description', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->filled('status')) {
|
||||
$query->where('is_active', $request->get('status') === 'active');
|
||||
}
|
||||
|
||||
$operators = $query->latest()->paginate(15);
|
||||
|
||||
return response()->json([
|
||||
'data' => OperatorResource::collection($operators),
|
||||
'meta' => [
|
||||
'current_page' => $operators->currentPage(),
|
||||
'last_page' => $operators->lastPage(),
|
||||
'per_page' => $operators->perPage(),
|
||||
'total' => $operators->total(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(OperatorRequest $request): JsonResponse
|
||||
{
|
||||
$operator = Operator::create($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Operator created successfully',
|
||||
'data' => new OperatorResource($operator)
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Operator $operator): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => new OperatorResource($operator)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(OperatorRequest $request, Operator $operator): JsonResponse
|
||||
{
|
||||
$operator->update($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Operator updated successfully',
|
||||
'data' => new OperatorResource($operator)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Operator $operator): JsonResponse
|
||||
{
|
||||
$operator->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Operator deleted successfully'
|
||||
]);
|
||||
}
|
||||
}
|
||||
95
app/Http/Controllers/Api/RideController.php
Normal file
95
app/Http/Controllers/Api/RideController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Ride;
|
||||
use App\Http\Requests\RideRequest;
|
||||
use App\Http\Resources\RideResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class RideController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Ride::query();
|
||||
|
||||
// Search functionality
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->get('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'ILIKE', "%{$search}%")
|
||||
->orWhere('description', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->filled('status')) {
|
||||
$query->where('is_active', $request->get('status') === 'active');
|
||||
}
|
||||
|
||||
$rides = $query->latest()->paginate(15);
|
||||
|
||||
return response()->json([
|
||||
'data' => RideResource::collection($rides),
|
||||
'meta' => [
|
||||
'current_page' => $rides->currentPage(),
|
||||
'last_page' => $rides->lastPage(),
|
||||
'per_page' => $rides->perPage(),
|
||||
'total' => $rides->total(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(RideRequest $request): JsonResponse
|
||||
{
|
||||
$ride = Ride::create($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Ride created successfully',
|
||||
'data' => new RideResource($ride)
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Ride $ride): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => new RideResource($ride)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(RideRequest $request, Ride $ride): JsonResponse
|
||||
{
|
||||
$ride->update($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Ride updated successfully',
|
||||
'data' => new RideResource($ride)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Ride $ride): JsonResponse
|
||||
{
|
||||
$ride->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Ride deleted successfully'
|
||||
]);
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
27
app/Http/Controllers/Auth/VerifyEmailController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
18
app/Http/Controllers/HomeController.php
Normal file
18
app/Http/Controllers/HomeController.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\ParkArea;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('home', [
|
||||
'total_parks' => Park::count(),
|
||||
'total_attractions' => ParkArea::count(),
|
||||
'total_coasters' => ParkArea::where('type', 'roller_coaster')->count(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
98
app/Http/Controllers/OperatorController.php
Normal file
98
app/Http/Controllers/OperatorController.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Operator;
|
||||
use App\Http\Requests\OperatorRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class OperatorController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = Operator::query();
|
||||
|
||||
// Search functionality
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->get('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'ILIKE', "%{$search}%")
|
||||
->orWhere('description', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->filled('status')) {
|
||||
$query->where('is_active', $request->get('status') === 'active');
|
||||
}
|
||||
|
||||
$operators = $query->latest()->paginate(15)->withQueryString();
|
||||
|
||||
return view('operators.index', compact('operators'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('operators.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(OperatorRequest $request): RedirectResponse
|
||||
{
|
||||
$operator = Operator::create($request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('operators.show', $operator)
|
||||
->with('success', 'Operator created successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Operator $operator): View
|
||||
{
|
||||
return view('operators.show', compact('operator'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Operator $operator): View
|
||||
{
|
||||
return view('operators.edit', compact('operator'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(OperatorRequest $request, Operator $operator): RedirectResponse
|
||||
{
|
||||
$operator->update($request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('operators.show', $operator)
|
||||
->with('success', 'Operator updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Operator $operator): RedirectResponse
|
||||
{
|
||||
$operator->delete();
|
||||
|
||||
return redirect()
|
||||
->route('operators.index')
|
||||
->with('success', 'Operator deleted successfully!');
|
||||
}
|
||||
}
|
||||
57
app/Http/Controllers/ParkController.php
Normal file
57
app/Http/Controllers/ParkController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Park;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ParkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of parks.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('parks.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new park.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('parks.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified park.
|
||||
*/
|
||||
public function show(Park $park): View
|
||||
{
|
||||
// Load relationships for the park detail page
|
||||
$park->load([
|
||||
'operator',
|
||||
'location',
|
||||
'areas.rides' => function ($query) {
|
||||
$query->orderBy('position')->orderBy('name');
|
||||
},
|
||||
'areas' => function ($query) {
|
||||
$query->orderBy('position')->orderBy('name');
|
||||
},
|
||||
'photos' => function ($query) {
|
||||
$query->orderBy('is_featured', 'desc')->orderBy('created_at', 'desc');
|
||||
}
|
||||
]);
|
||||
|
||||
return view('parks.show', compact('park'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified park.
|
||||
*/
|
||||
public function edit(Park $park): View
|
||||
{
|
||||
return view('parks.edit', compact('park'));
|
||||
}
|
||||
}
|
||||
212
app/Http/Controllers/PhotoController.php
Normal file
212
app/Http/Controllers/PhotoController.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Photo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
use Intervention\Image\Encoders\JpegEncoder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PhotoController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the photos for a park.
|
||||
*/
|
||||
public function index(Park $park)
|
||||
{
|
||||
$photos = $park->photos()->ordered()->get();
|
||||
|
||||
return response()->json([
|
||||
'photos' => $photos,
|
||||
'featured_photo' => $park->featuredPhoto(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly uploaded photo.
|
||||
*/
|
||||
public function store(Request $request, Park $park)
|
||||
{
|
||||
$request->validate([
|
||||
'photo' => 'required|image|max:10240', // 10MB max
|
||||
'title' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'alt_text' => 'nullable|string|max:255',
|
||||
'credit' => 'nullable|string|max:255',
|
||||
'source_url' => 'nullable|url|max:255',
|
||||
'is_featured' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
// Handle file upload
|
||||
if ($request->hasFile('photo')) {
|
||||
$file = $request->file('photo');
|
||||
$originalFilename = $file->getClientOriginalName();
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
|
||||
// Generate a unique filename
|
||||
$filename = time() . '_' . Str::slug(pathinfo($originalFilename, PATHINFO_FILENAME)) . '.' . $extension;
|
||||
|
||||
// Define the storage path
|
||||
$storagePath = 'photos/parks/' . $park->id;
|
||||
$fullPath = $storagePath . '/' . $filename;
|
||||
|
||||
// Store the original file
|
||||
$file->storeAs('public/' . $storagePath, $filename);
|
||||
|
||||
// Create image manager instance
|
||||
$manager = new ImageManager(new Driver());
|
||||
|
||||
// Get image dimensions
|
||||
$image = $manager->read($file);
|
||||
$width = $image->width();
|
||||
$height = $image->height();
|
||||
|
||||
// Generate thumbnail
|
||||
$thumbnailFilename = pathinfo($filename, PATHINFO_FILENAME) . '_thumb.' . $extension;
|
||||
$thumbnail = $manager->read($file)->scaleDown(width: 300, height: 300);
|
||||
|
||||
// Save thumbnail
|
||||
$encodedThumbnail = $thumbnail->encode(
|
||||
new JpegEncoder(80)
|
||||
);
|
||||
Storage::put(
|
||||
'public/' . $storagePath . '/' . $thumbnailFilename,
|
||||
$encodedThumbnail->toString()
|
||||
);
|
||||
|
||||
// Create photo record
|
||||
$photo = $park->addPhoto([
|
||||
'title' => $request->input('title'),
|
||||
'description' => $request->input('description'),
|
||||
'file_path' => $fullPath,
|
||||
'file_name' => $originalFilename,
|
||||
'file_size' => $file->getSize(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'alt_text' => $request->input('alt_text'),
|
||||
'credit' => $request->input('credit'),
|
||||
'source_url' => $request->input('source_url'),
|
||||
'is_featured' => $request->input('is_featured', false),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Photo uploaded successfully',
|
||||
'photo' => $photo,
|
||||
], 201);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'No photo file provided',
|
||||
], 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified photo.
|
||||
*/
|
||||
public function show(Photo $photo)
|
||||
{
|
||||
return response()->json($photo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified photo.
|
||||
*/
|
||||
public function update(Request $request, Photo $photo)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'alt_text' => 'nullable|string|max:255',
|
||||
'credit' => 'nullable|string|max:255',
|
||||
'source_url' => 'nullable|url|max:255',
|
||||
'is_featured' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$photo->update($request->only([
|
||||
'title', 'description', 'alt_text', 'credit', 'source_url'
|
||||
]));
|
||||
|
||||
// Handle featured status
|
||||
if ($request->has('is_featured') && $request->input('is_featured')) {
|
||||
$photo->setAsFeatured();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Photo updated successfully',
|
||||
'photo' => $photo,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified photo.
|
||||
*/
|
||||
public function destroy(Photo $photo)
|
||||
{
|
||||
// Get the file paths
|
||||
$filePath = 'public/' . $photo->file_path;
|
||||
$pathInfo = pathinfo($photo->file_path);
|
||||
$thumbnailPath = 'public/' . $pathInfo['dirname'] . '/' . $pathInfo['filename'] . '_thumb.' . $pathInfo['extension'];
|
||||
|
||||
// Delete the files
|
||||
Storage::delete([$filePath, $thumbnailPath]);
|
||||
|
||||
// Delete the record
|
||||
$photo->delete();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Photo deleted successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder photos for a park.
|
||||
*/
|
||||
public function reorder(Request $request, Park $park)
|
||||
{
|
||||
$request->validate([
|
||||
'photo_ids' => 'required|array',
|
||||
'photo_ids.*' => 'required|integer|exists:photos,id',
|
||||
]);
|
||||
|
||||
$success = $park->reorderPhotos($request->input('photo_ids'));
|
||||
|
||||
if ($success) {
|
||||
return response()->json([
|
||||
'message' => 'Photos reordered successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Failed to reorder photos',
|
||||
], 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a photo as featured.
|
||||
*/
|
||||
public function setFeatured(Request $request, Park $park, Photo $photo)
|
||||
{
|
||||
if ($photo->photoable_id !== $park->id || $photo->photoable_type !== Park::class) {
|
||||
return response()->json([
|
||||
'message' => 'Photo does not belong to this park',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$success = $park->setFeaturedPhoto($photo);
|
||||
|
||||
if ($success) {
|
||||
return response()->json([
|
||||
'message' => 'Featured photo set successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Failed to set featured photo',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
98
app/Http/Controllers/RideController.php
Normal file
98
app/Http/Controllers/RideController.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Http\Requests\RideRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class RideController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = Ride::query();
|
||||
|
||||
// Search functionality
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->get('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'ILIKE', "%{$search}%")
|
||||
->orWhere('description', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->filled('status')) {
|
||||
$query->where('is_active', $request->get('status') === 'active');
|
||||
}
|
||||
|
||||
$rides = $query->latest()->paginate(15)->withQueryString();
|
||||
|
||||
return view('rides.index', compact('rides'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('rides.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(RideRequest $request): RedirectResponse
|
||||
{
|
||||
$ride = Ride::create($request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('rides.show', $ride)
|
||||
->with('success', 'Ride created successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Ride $ride): View
|
||||
{
|
||||
return view('rides.show', compact('ride'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Ride $ride): View
|
||||
{
|
||||
return view('rides.edit', compact('ride'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(RideRequest $request, Ride $ride): RedirectResponse
|
||||
{
|
||||
$ride->update($request->validated());
|
||||
|
||||
return redirect()
|
||||
->route('rides.show', $ride)
|
||||
->with('success', 'Ride updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Ride $ride): RedirectResponse
|
||||
{
|
||||
$ride->delete();
|
||||
|
||||
return redirect()
|
||||
->route('rides.index')
|
||||
->with('success', 'Ride deleted successfully!');
|
||||
}
|
||||
}
|
||||
65
app/Http/Kernel.php
Normal file
65
app/Http/Kernel.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* @var array<int, class-string|string>
|
||||
*/
|
||||
protected $middleware = [
|
||||
// \App\Http\Middleware\TrustHosts::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array<string, array<int, class-string|string>>
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's middleware aliases.
|
||||
*
|
||||
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
|
||||
*
|
||||
* @var array<string, class-string|string>
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
|
||||
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
];
|
||||
}
|
||||
17
app/Http/Middleware/Authenticate.php
Normal file
17
app/Http/Middleware/Authenticate.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Authenticate extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the path the user should be redirected to when they are not authenticated.
|
||||
*/
|
||||
protected function redirectTo(Request $request): ?string
|
||||
{
|
||||
return $request->expectsJson() ? null : route('login');
|
||||
}
|
||||
}
|
||||
12
app/Http/Middleware/EncryptCookies.php
Normal file
12
app/Http/Middleware/EncryptCookies.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
||||
|
||||
class EncryptCookies extends Middleware
|
||||
{
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
12
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file
12
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
|
||||
|
||||
class PreventRequestsDuringMaintenance extends Middleware
|
||||
{
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
28
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file
28
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RedirectIfAuthenticated
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||
{
|
||||
$guards = empty($guards) ? [null] : $guards;
|
||||
|
||||
foreach ($guards as $guard) {
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
14
app/Http/Middleware/TrimStrings.php
Normal file
14
app/Http/Middleware/TrimStrings.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
||||
|
||||
class TrimStrings extends Middleware
|
||||
{
|
||||
protected $except = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
}
|
||||
20
app/Http/Middleware/TrustHosts.php
Normal file
20
app/Http/Middleware/TrustHosts.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||
|
||||
class TrustHosts extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the host patterns that should be trusted.
|
||||
*
|
||||
* @return array<int, string|null>
|
||||
*/
|
||||
public function hosts(): array
|
||||
{
|
||||
return [
|
||||
$this->allSubdomainsOfApplicationUrl(),
|
||||
];
|
||||
}
|
||||
}
|
||||
17
app/Http/Middleware/TrustProxies.php
Normal file
17
app/Http/Middleware/TrustProxies.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustProxies as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TrustProxies extends Middleware
|
||||
{
|
||||
protected $proxies;
|
||||
protected $headers =
|
||||
Request::HEADER_X_FORWARDED_FOR |
|
||||
Request::HEADER_X_FORWARDED_HOST |
|
||||
Request::HEADER_X_FORWARDED_PORT |
|
||||
Request::HEADER_X_FORWARDED_PROTO |
|
||||
Request::HEADER_X_FORWARDED_AWS_ELB;
|
||||
}
|
||||
22
app/Http/Middleware/ValidateSignature.php
Normal file
22
app/Http/Middleware/ValidateSignature.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
|
||||
|
||||
class ValidateSignature extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the query string parameters that should be ignored.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
// 'fbclid',
|
||||
// 'utm_campaign',
|
||||
// 'utm_content',
|
||||
// 'utm_medium',
|
||||
// 'utm_source',
|
||||
// 'utm_term',
|
||||
];
|
||||
}
|
||||
12
app/Http/Middleware/VerifyCsrfToken.php
Normal file
12
app/Http/Middleware/VerifyCsrfToken.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
48
app/Http/Requests/OperatorRequest.php
Normal file
48
app/Http/Requests/OperatorRequest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class OperatorRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Add authorization logic as needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
|
||||
// For updates, make name unique except for current record
|
||||
if ($this->route('operator')) {
|
||||
$rules['name'][] = 'unique:operators,name,' . $this->route('operator')->id;
|
||||
} else {
|
||||
$rules['name'][] = 'unique:operators,name';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'The operator name is required.',
|
||||
'name.unique' => 'A operator with this name already exists.',
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/Http/Requests/ParkRequest.php
Normal file
48
app/Http/Requests/ParkRequest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ParkRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Add authorization logic as needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
|
||||
// For updates, make name unique except for current record
|
||||
if ($this->route('park')) {
|
||||
$rules['name'][] = 'unique:parks,name,' . $this->route('park')->id;
|
||||
} else {
|
||||
$rules['name'][] = 'unique:parks,name';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'The park name is required.',
|
||||
'name.unique' => 'A park with this name already exists.',
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/Http/Requests/RideRequest.php
Normal file
48
app/Http/Requests/RideRequest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RideRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Add authorization logic as needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
|
||||
// For updates, make name unique except for current record
|
||||
if ($this->route('ride')) {
|
||||
$rules['name'][] = 'unique:rides,name,' . $this->route('ride')->id;
|
||||
} else {
|
||||
$rules['name'][] = 'unique:rides,name';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'The ride name is required.',
|
||||
'name.unique' => 'A ride with this name already exists.',
|
||||
];
|
||||
}
|
||||
}
|
||||
53
app/Http/Resources/ManufacturerResource.php
Normal file
53
app/Http/Resources/ManufacturerResource.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* Manufacturer API Resource
|
||||
*
|
||||
* Transforms Manufacturer model data for API responses
|
||||
* Includes ThrillWiki optimization patterns
|
||||
*/
|
||||
class ManufacturerResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'description' => $this->description,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
|
||||
// Include relationships when loaded
|
||||
$this->mergeWhen($this->relationLoaded('relationships'), [
|
||||
// Add relationship data here
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional data that should be returned with the resource array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function with(Request $request): array
|
||||
{
|
||||
return [
|
||||
'meta' => [
|
||||
'model' => 'Manufacturer',
|
||||
'generated_at' => now()->toISOString(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Http/Resources/OperatorResource.php
Normal file
24
app/Http/Resources/OperatorResource.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class OperatorResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Http/Resources/RideResource.php
Normal file
24
app/Http/Resources/RideResource.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class RideResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
20
app/Livewire/Actions/Logout.php
Normal file
20
app/Livewire/Actions/Logout.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Actions;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class Logout
|
||||
{
|
||||
/**
|
||||
* Log the current user out of the application.
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
Session::invalidate();
|
||||
Session::regenerateToken();
|
||||
}
|
||||
}
|
||||
25
app/Livewire/AuthMenuComponent.php
Normal file
25
app/Livewire/AuthMenuComponent.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class AuthMenuComponent extends Component
|
||||
{
|
||||
public bool $isOpen = false;
|
||||
|
||||
public function toggle()
|
||||
{
|
||||
$this->isOpen = !$this->isOpen;
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$this->isOpen = false;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.auth-menu-component');
|
||||
}
|
||||
}
|
||||
87
app/Livewire/AutocompleteComponent.php
Normal file
87
app/Livewire/AutocompleteComponent.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Ride;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Livewire\Component;
|
||||
|
||||
class AutocompleteComponent extends Component
|
||||
{
|
||||
public string $query = '';
|
||||
public string $type = 'park';
|
||||
public array $suggestions = [];
|
||||
public ?string $selectedId = null;
|
||||
|
||||
protected $queryString = [
|
||||
'query' => ['except' => ''],
|
||||
'type' => ['except' => 'park']
|
||||
];
|
||||
|
||||
public function mount(string $type = 'park'): void
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.autocomplete', [
|
||||
'suggestions' => $this->suggestions
|
||||
]);
|
||||
}
|
||||
|
||||
public function updatedQuery(): void
|
||||
{
|
||||
if (strlen($this->query) < 2) {
|
||||
$this->suggestions = [];
|
||||
return;
|
||||
}
|
||||
|
||||
$this->suggestions = match ($this->type) {
|
||||
'park' => $this->getParkSuggestions(),
|
||||
'ride' => $this->getRideSuggestions(),
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
protected function getParkSuggestions(): array
|
||||
{
|
||||
return Park::query()
|
||||
->select(['id', 'name', 'slug'])
|
||||
->where('name', 'like', "%{$this->query}%")
|
||||
->orderBy('name')
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(fn($park) => [
|
||||
'id' => $park->id,
|
||||
'text' => $park->name,
|
||||
'url' => route('parks.show', $park->slug)
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
protected function getRideSuggestions(): array
|
||||
{
|
||||
return Ride::query()
|
||||
->select(['id', 'name', 'slug', 'park_id'])
|
||||
->with('park:id,name')
|
||||
->where('name', 'like', "%{$this->query}%")
|
||||
->orderBy('name')
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(fn($ride) => [
|
||||
'id' => $ride->id,
|
||||
'text' => "{$ride->name} at {$ride->park->name}",
|
||||
'url' => route('rides.show', $ride->slug)
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function selectSuggestion(string $id): void
|
||||
{
|
||||
$this->selectedId = $id;
|
||||
$this->dispatch('suggestion-selected', id: $id);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ class Counter extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.counter');
|
||||
return view('livewire.counter')
|
||||
->layout('layouts.app');
|
||||
}
|
||||
}
|
||||
|
||||
563
app/Livewire/DesignersListingUniversal.php
Normal file
563
app/Livewire/DesignersListingUniversal.php
Normal file
@@ -0,0 +1,563 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Designer;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DesignersListingUniversal extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Universal Listing System Integration
|
||||
public string $entityType = 'designers';
|
||||
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'specialties')]
|
||||
public array $specialties = [];
|
||||
|
||||
#[Url(as: 'style')]
|
||||
public string $designStyle = '';
|
||||
|
||||
#[Url(as: 'founded_from')]
|
||||
public string $foundedYearFrom = '';
|
||||
|
||||
#[Url(as: 'founded_to')]
|
||||
public string $foundedYearTo = '';
|
||||
|
||||
#[Url(as: 'innovation_min')]
|
||||
public string $minInnovationScore = '';
|
||||
|
||||
#[Url(as: 'innovation_max')]
|
||||
public string $maxInnovationScore = '';
|
||||
|
||||
#[Url(as: 'active_years_min')]
|
||||
public string $minActiveYears = '';
|
||||
|
||||
#[Url(as: 'active_years_max')]
|
||||
public string $maxActiveYears = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'dir')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
#[Url(as: 'view')]
|
||||
public string $viewMode = 'grid';
|
||||
|
||||
public int $perPage = 20;
|
||||
public array $portfolioStats = [];
|
||||
public array $innovationTimeline = [];
|
||||
public array $collaborationNetworks = [];
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'specialties' => ['except' => []],
|
||||
'designStyle' => ['except' => ''],
|
||||
'foundedYearFrom' => ['except' => ''],
|
||||
'foundedYearTo' => ['except' => ''],
|
||||
'minInnovationScore' => ['except' => ''],
|
||||
'maxInnovationScore' => ['except' => ''],
|
||||
'minActiveYears' => ['except' => ''],
|
||||
'maxActiveYears' => ['except' => ''],
|
||||
'sortBy' => ['except' => 'name'],
|
||||
'sortDirection' => ['except' => 'asc'],
|
||||
'viewMode' => ['except' => 'grid'],
|
||||
'page' => ['except' => 1],
|
||||
];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadPortfolioStatistics();
|
||||
$this->loadInnovationTimeline();
|
||||
$this->loadCollaborationNetworks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load creative portfolio statistics with caching
|
||||
*/
|
||||
protected function loadPortfolioStatistics(): void
|
||||
{
|
||||
$this->portfolioStats = Cache::remember(
|
||||
'designers.portfolio.stats',
|
||||
now()->addHours(6),
|
||||
fn() => $this->calculatePortfolioStatistics()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load innovation timeline data with caching
|
||||
*/
|
||||
protected function loadInnovationTimeline(): void
|
||||
{
|
||||
$this->innovationTimeline = Cache::remember(
|
||||
'designers.innovation.timeline',
|
||||
now()->addHours(12),
|
||||
fn() => $this->calculateInnovationTimeline()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load collaboration networks with caching
|
||||
*/
|
||||
protected function loadCollaborationNetworks(): void
|
||||
{
|
||||
$this->collaborationNetworks = Cache::remember(
|
||||
'designers.collaboration.networks',
|
||||
now()->addHours(6),
|
||||
fn() => $this->calculateCollaborationNetworks()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate comprehensive portfolio statistics
|
||||
*/
|
||||
protected function calculatePortfolioStatistics(): array
|
||||
{
|
||||
return [
|
||||
'total_designers' => Designer::active()->count(),
|
||||
'coaster_designers' => Designer::active()->specialty('roller_coaster')->count(),
|
||||
'dark_ride_designers' => Designer::active()->specialty('dark_ride')->count(),
|
||||
'themed_experience_designers' => Designer::active()->specialty('themed_experience')->count(),
|
||||
'water_attraction_designers' => Designer::active()->specialty('water_attraction')->count(),
|
||||
'specialties_distribution' => Designer::active()
|
||||
->select('specialty', DB::raw('count(*) as count'))
|
||||
->whereNotNull('specialty')
|
||||
->groupBy('specialty')
|
||||
->orderByDesc('count')
|
||||
->get()
|
||||
->pluck('count', 'specialty')
|
||||
->toArray(),
|
||||
'average_innovation_score' => Designer::active()
|
||||
->whereNotNull('innovation_score')
|
||||
->avg('innovation_score'),
|
||||
'total_designs' => Designer::active()
|
||||
->withCount('rides')
|
||||
->get()
|
||||
->sum('rides_count'),
|
||||
'top_innovators' => Designer::active()
|
||||
->orderByDesc('innovation_score')
|
||||
->take(5)
|
||||
->get(['name', 'innovation_score', 'specialty'])
|
||||
->toArray(),
|
||||
'design_styles' => Designer::active()
|
||||
->select('design_style', DB::raw('count(*) as count'))
|
||||
->whereNotNull('design_style')
|
||||
->groupBy('design_style')
|
||||
->orderByDesc('count')
|
||||
->get()
|
||||
->pluck('count', 'design_style')
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate innovation timeline data
|
||||
*/
|
||||
protected function calculateInnovationTimeline(): array
|
||||
{
|
||||
$timelineData = Designer::active()
|
||||
->whereNotNull('founded_year')
|
||||
->whereNotNull('innovation_score')
|
||||
->select('founded_year', 'innovation_score', 'name', 'specialty')
|
||||
->orderBy('founded_year')
|
||||
->get()
|
||||
->groupBy(function($designer) {
|
||||
return floor($designer->founded_year / 10) * 10; // Group by decade
|
||||
})
|
||||
->map(function($decade) {
|
||||
return [
|
||||
'count' => $decade->count(),
|
||||
'avg_innovation' => $decade->avg('innovation_score'),
|
||||
'top_designer' => $decade->sortByDesc('innovation_score')->first(),
|
||||
'specialties' => $decade->countBy('specialty')->toArray()
|
||||
];
|
||||
});
|
||||
|
||||
return [
|
||||
'timeline' => $timelineData->toArray(),
|
||||
'innovation_milestones' => Designer::active()
|
||||
->where('innovation_score', '>=', 8.5)
|
||||
->orderByDesc('innovation_score')
|
||||
->take(10)
|
||||
->get(['name', 'founded_year', 'innovation_score', 'specialty'])
|
||||
->toArray(),
|
||||
'breakthrough_years' => Designer::active()
|
||||
->whereNotNull('founded_year')
|
||||
->select('founded_year', DB::raw('count(*) as new_designers'), DB::raw('avg(innovation_score) as avg_innovation'))
|
||||
->groupBy('founded_year')
|
||||
->having('new_designers', '>=', 2)
|
||||
->orderByDesc('avg_innovation')
|
||||
->take(5)
|
||||
->get()
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate collaboration networks
|
||||
*/
|
||||
protected function calculateCollaborationNetworks(): array
|
||||
{
|
||||
return [
|
||||
'collaboration_pairs' => Designer::active()
|
||||
->whereHas('rides', function($query) {
|
||||
$query->whereHas('park', function($parkQuery) {
|
||||
$parkQuery->whereHas('rides', function($rideQuery) {
|
||||
$rideQuery->whereNotNull('designer_id');
|
||||
});
|
||||
});
|
||||
})
|
||||
->with(['rides.park.rides.designer'])
|
||||
->get()
|
||||
->flatMap(function($designer) {
|
||||
return $designer->rides->flatMap(function($ride) use ($designer) {
|
||||
return $ride->park->rides
|
||||
->where('designer_id', '!=', $designer->id)
|
||||
->whereNotNull('designer_id')
|
||||
->pluck('designer.name')
|
||||
->map(function($collaborator) use ($designer, $ride) {
|
||||
return [
|
||||
'designer' => $designer->name,
|
||||
'collaborator' => $collaborator,
|
||||
'park' => $ride->park->name
|
||||
];
|
||||
});
|
||||
});
|
||||
})
|
||||
->groupBy(function($item) {
|
||||
$names = [$item['designer'], $item['collaborator']];
|
||||
sort($names);
|
||||
return implode(' + ', $names);
|
||||
})
|
||||
->map(function($collaborations) {
|
||||
return [
|
||||
'count' => $collaborations->count(),
|
||||
'parks' => $collaborations->pluck('park')->unique()->values()->toArray()
|
||||
];
|
||||
})
|
||||
->sortByDesc('count')
|
||||
->take(10)
|
||||
->toArray(),
|
||||
'network_hubs' => Designer::active()
|
||||
->withCount('rides')
|
||||
->having('rides_count', '>=', 3)
|
||||
->orderByDesc('rides_count')
|
||||
->take(10)
|
||||
->get(['name', 'specialty', 'rides_count'])
|
||||
->toArray(),
|
||||
'cross_specialty_projects' => Designer::active()
|
||||
->whereHas('rides', function($query) {
|
||||
$query->whereHas('park', function($parkQuery) {
|
||||
$parkQuery->whereHas('rides', function($rideQuery) {
|
||||
$rideQuery->whereHas('designer', function($designerQuery) {
|
||||
$designerQuery->whereColumn('specialty', '!=', 'designers.specialty');
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
->with(['rides.park'])
|
||||
->get()
|
||||
->flatMap(function($designer) {
|
||||
return $designer->rides->map(function($ride) use ($designer) {
|
||||
return [
|
||||
'designer' => $designer->name,
|
||||
'specialty' => $designer->specialty,
|
||||
'park' => $ride->park->name,
|
||||
'ride' => $ride->name
|
||||
];
|
||||
});
|
||||
})
|
||||
->take(15)
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Django parity creative portfolio search functionality
|
||||
*/
|
||||
public function creativePortfolioSearch($query, $specialties = [])
|
||||
{
|
||||
return Designer::query()
|
||||
->when($query, function ($q) use ($query) {
|
||||
$terms = explode(' ', trim($query));
|
||||
foreach ($terms as $term) {
|
||||
if (strlen($term) >= 2) {
|
||||
$q->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhere('specialty', 'ilike', "%{$term}%")
|
||||
->orWhere('design_style', 'ilike', "%{$term}%")
|
||||
->orWhere('headquarters', 'ilike', "%{$term}%")
|
||||
->orWhereHas('rides', function($rideQuery) use ($term) {
|
||||
$rideQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('category', 'ilike', "%{$term}%");
|
||||
})
|
||||
->orWhereHas('rides.park', function($parkQuery) use ($term) {
|
||||
$parkQuery->where('name', 'ilike', "%{$term}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
->when($specialties, function ($q) use ($specialties) {
|
||||
$q->whereIn('specialty', $specialties);
|
||||
})
|
||||
->active()
|
||||
->with(['rides:id,designer_id,name,category,park_id', 'rides.park:id,name'])
|
||||
->withCount(['rides']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply creative and innovation filters
|
||||
*/
|
||||
public function applyCreativeFilters($query)
|
||||
{
|
||||
return $query
|
||||
->when($this->designStyle, fn($q, $style) =>
|
||||
$q->where('design_style', $style))
|
||||
->when($this->foundedYearFrom, fn($q, $year) =>
|
||||
$q->where('founded_year', '>=', $year))
|
||||
->when($this->foundedYearTo, fn($q, $year) =>
|
||||
$q->where('founded_year', '<=', $year))
|
||||
->when($this->minInnovationScore, fn($q, $score) =>
|
||||
$q->where('innovation_score', '>=', $score))
|
||||
->when($this->maxInnovationScore, fn($q, $score) =>
|
||||
$q->where('innovation_score', '<=', $score))
|
||||
->when($this->minActiveYears, fn($q, $years) =>
|
||||
$q->where('active_years', '>=', $years))
|
||||
->when($this->maxActiveYears, fn($q, $years) =>
|
||||
$q->where('active_years', '<=', $years));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get designers with optimized caching
|
||||
*/
|
||||
public function getDesignersProperty()
|
||||
{
|
||||
$cacheKey = "designers.listing." . md5(serialize([
|
||||
'search' => $this->search,
|
||||
'specialties' => $this->specialties,
|
||||
'designStyle' => $this->designStyle,
|
||||
'foundedYearFrom' => $this->foundedYearFrom,
|
||||
'foundedYearTo' => $this->foundedYearTo,
|
||||
'minInnovationScore' => $this->minInnovationScore,
|
||||
'maxInnovationScore' => $this->maxInnovationScore,
|
||||
'minActiveYears' => $this->minActiveYears,
|
||||
'maxActiveYears' => $this->maxActiveYears,
|
||||
'sortBy' => $this->sortBy,
|
||||
'sortDirection' => $this->sortDirection,
|
||||
'page' => $this->getPage(),
|
||||
'perPage' => $this->perPage,
|
||||
]));
|
||||
|
||||
return Cache::remember($cacheKey, now()->addMinutes(30), function() {
|
||||
$query = $this->creativePortfolioSearch($this->search, $this->specialties);
|
||||
$query = $this->applyCreativeFilters($query);
|
||||
|
||||
// Apply sorting
|
||||
switch ($this->sortBy) {
|
||||
case 'name':
|
||||
$query->orderBy('name', $this->sortDirection);
|
||||
break;
|
||||
case 'founded_year':
|
||||
$query->orderBy('founded_year', $this->sortDirection);
|
||||
break;
|
||||
case 'innovation_score':
|
||||
$query->orderBy('innovation_score', $this->sortDirection);
|
||||
break;
|
||||
case 'designed_rides_count':
|
||||
$query->orderBy('rides_count', $this->sortDirection);
|
||||
break;
|
||||
case 'active_years':
|
||||
$query->orderBy('active_years', $this->sortDirection);
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
return $query->paginate($this->perPage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search and reset pagination
|
||||
*/
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update specialties filter and reset pagination
|
||||
*/
|
||||
public function updatedSpecialties(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any filter and reset pagination
|
||||
*/
|
||||
public function updatedDesignStyle(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearFrom(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearTo(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMinInnovationScore(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMaxInnovationScore(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMinActiveYears(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMaxActiveYears(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by specific column
|
||||
*/
|
||||
public function sortBy(string $column): void
|
||||
{
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change view mode
|
||||
*/
|
||||
public function setViewMode(string $mode): void
|
||||
{
|
||||
$this->viewMode = $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->search = '';
|
||||
$this->specialties = [];
|
||||
$this->designStyle = '';
|
||||
$this->foundedYearFrom = '';
|
||||
$this->foundedYearTo = '';
|
||||
$this->minInnovationScore = '';
|
||||
$this->maxInnovationScore = '';
|
||||
$this->minActiveYears = '';
|
||||
$this->maxActiveYears = '';
|
||||
$this->sortBy = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle specialty filter
|
||||
*/
|
||||
public function toggleSpecialtyFilter(string $specialty): void
|
||||
{
|
||||
if (in_array($specialty, $this->specialties)) {
|
||||
$this->specialties = array_values(array_diff($this->specialties, [$specialty]));
|
||||
} else {
|
||||
$this->specialties[] = $specialty;
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(): void
|
||||
{
|
||||
Cache::forget('designers.portfolio.stats');
|
||||
Cache::forget('designers.innovation.timeline');
|
||||
Cache::forget('designers.collaboration.networks');
|
||||
|
||||
// Clear listing cache pattern - simplified approach
|
||||
$cacheKeys = [
|
||||
'designers.listing.*'
|
||||
];
|
||||
|
||||
foreach ($cacheKeys as $pattern) {
|
||||
Cache::forget($pattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.designers-listing-universal', [
|
||||
'designers' => $this->designers,
|
||||
'portfolioStats' => $this->portfolioStats,
|
||||
'innovationTimeline' => $this->innovationTimeline,
|
||||
'collaborationNetworks' => $this->collaborationNetworks,
|
||||
]);
|
||||
}
|
||||
}
|
||||
89
app/Livewire/FeaturedPhotoSelectorComponent.php
Normal file
89
app/Livewire/FeaturedPhotoSelectorComponent.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Photo;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FeaturedPhotoSelectorComponent extends Component
|
||||
{
|
||||
public Park $park;
|
||||
public $photos = [];
|
||||
public $featuredPhotoId = null;
|
||||
public $isLoading = true;
|
||||
public $error = null;
|
||||
public $success = false;
|
||||
|
||||
protected $listeners = ['photoUploaded' => 'loadPhotos'];
|
||||
|
||||
public function mount(Park $park)
|
||||
{
|
||||
$this->park = $park;
|
||||
$this->loadPhotos();
|
||||
}
|
||||
|
||||
public function loadPhotos()
|
||||
{
|
||||
$this->isLoading = true;
|
||||
$this->error = null;
|
||||
$this->success = false;
|
||||
|
||||
try {
|
||||
$this->photos = $this->park->photos()->ordered()->get();
|
||||
$featuredPhoto = $this->park->featuredPhoto();
|
||||
$this->featuredPhotoId = $featuredPhoto ? $featuredPhoto->id : null;
|
||||
$this->isLoading = false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error loading photos: ' . $e->getMessage());
|
||||
$this->error = 'Failed to load photos: ' . $e->getMessage();
|
||||
$this->isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function setFeatured($photoId)
|
||||
{
|
||||
$this->isLoading = true;
|
||||
$this->error = null;
|
||||
$this->success = false;
|
||||
|
||||
try {
|
||||
$photo = Photo::findOrFail($photoId);
|
||||
|
||||
if ($photo->photoable_id !== $this->park->id || $photo->photoable_type !== Park::class) {
|
||||
throw new \Exception('Photo does not belong to this park');
|
||||
}
|
||||
|
||||
$success = $this->park->setFeaturedPhoto($photo);
|
||||
|
||||
if ($success) {
|
||||
$this->featuredPhotoId = $photoId;
|
||||
$this->success = true;
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'success',
|
||||
'message' => 'Featured photo updated successfully'
|
||||
]);
|
||||
|
||||
// Emit event to refresh other components
|
||||
$this->dispatch('featuredPhotoChanged');
|
||||
} else {
|
||||
throw new \Exception('Failed to set featured photo');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error setting featured photo: ' . $e->getMessage());
|
||||
$this->error = 'Failed to set featured photo: ' . $e->getMessage();
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'error',
|
||||
'message' => 'Failed to set featured photo'
|
||||
]);
|
||||
} finally {
|
||||
$this->isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.featured-photo-selector-component');
|
||||
}
|
||||
}
|
||||
72
app/Livewire/Forms/LoginForm.php
Normal file
72
app/Livewire/Forms/LoginForm.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Form;
|
||||
|
||||
class LoginForm extends Form
|
||||
{
|
||||
#[Validate('required|string|email')]
|
||||
public string $email = '';
|
||||
|
||||
#[Validate('required|string')]
|
||||
public string $password = '';
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $remember = false;
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'form.email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the authentication request is not rate limited.
|
||||
*/
|
||||
protected function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout(request()));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'form.email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication rate limiting throttle key.
|
||||
*/
|
||||
protected function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
|
||||
}
|
||||
}
|
||||
54
app/Livewire/GlobalSearchComponent.php
Normal file
54
app/Livewire/GlobalSearchComponent.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class GlobalSearchComponent extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.global-search-component');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
118
app/Livewire/Location/LocationDisplayComponent.php
Normal file
118
app/Livewire/Location/LocationDisplayComponent.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Location;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Location;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class LocationDisplayComponent extends Component
|
||||
{
|
||||
// Map Configuration
|
||||
public array $markers = [];
|
||||
public bool $showClusters = true;
|
||||
public int $clusterRadius = 50;
|
||||
public int $defaultZoom = 13;
|
||||
public ?array $bounds = null;
|
||||
|
||||
// Display Settings
|
||||
public bool $showInfoWindow = false;
|
||||
public ?array $activeMarker = null;
|
||||
public array $customMarkers = [];
|
||||
public array $markerCategories = [];
|
||||
|
||||
// State Management
|
||||
public bool $isLoading = true;
|
||||
public ?string $error = null;
|
||||
public array $visibleMarkers = [];
|
||||
|
||||
// Lifecycle Hooks
|
||||
public function mount(array $markers = [], ?array $bounds = null, array $categories = [])
|
||||
{
|
||||
$this->markers = $markers;
|
||||
$this->bounds = $bounds;
|
||||
$this->markerCategories = $categories;
|
||||
$this->visibleMarkers = $markers;
|
||||
}
|
||||
|
||||
// Event Handlers
|
||||
public function markerClicked($markerId)
|
||||
{
|
||||
$this->activeMarker = collect($this->markers)->firstWhere('id', $markerId);
|
||||
$this->showInfoWindow = true;
|
||||
}
|
||||
|
||||
public function clusterClicked($clusterMarkers)
|
||||
{
|
||||
if (count($clusterMarkers) === 1) {
|
||||
$this->markerClicked($clusterMarkers[0]['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->bounds = $this->calculateBounds($clusterMarkers);
|
||||
}
|
||||
|
||||
public function closeInfoWindow()
|
||||
{
|
||||
$this->showInfoWindow = false;
|
||||
$this->activeMarker = null;
|
||||
}
|
||||
|
||||
public function boundsChanged($bounds)
|
||||
{
|
||||
$this->bounds = $bounds;
|
||||
$this->visibleMarkers = $this->getVisibleMarkers($bounds);
|
||||
}
|
||||
|
||||
public function updateMarkersVisibility($visible)
|
||||
{
|
||||
$this->visibleMarkers = collect($this->markers)
|
||||
->filter(fn ($marker) => in_array($marker['id'], $visible))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
// Helper Methods
|
||||
protected function calculateBounds($markers): array
|
||||
{
|
||||
if (empty($markers)) {
|
||||
return [
|
||||
'north' => 0,
|
||||
'south' => 0,
|
||||
'east' => 0,
|
||||
'west' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$lats = array_column($markers, 'lat');
|
||||
$lons = array_column($markers, 'lng');
|
||||
|
||||
return [
|
||||
'north' => max($lats),
|
||||
'south' => min($lats),
|
||||
'east' => max($lons),
|
||||
'west' => min($lons),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getVisibleMarkers($bounds): array
|
||||
{
|
||||
if (!$bounds) {
|
||||
return $this->markers;
|
||||
}
|
||||
|
||||
return collect($this->markers)
|
||||
->filter(function ($marker) use ($bounds) {
|
||||
return $marker['lat'] >= $bounds['south'] &&
|
||||
$marker['lat'] <= $bounds['north'] &&
|
||||
$marker['lng'] >= $bounds['west'] &&
|
||||
$marker['lng'] <= $bounds['east'];
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.location.location-display');
|
||||
}
|
||||
}
|
||||
162
app/Livewire/Location/LocationMapComponent.php
Normal file
162
app/Livewire/Location/LocationMapComponent.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Location;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Reactive;
|
||||
use App\Services\GeocodeService;
|
||||
|
||||
class LocationMapComponent extends Component
|
||||
{
|
||||
/**
|
||||
* The initial latitude for the map center
|
||||
*/
|
||||
#[Reactive]
|
||||
public ?float $latitude = null;
|
||||
|
||||
/**
|
||||
* The initial longitude for the map center
|
||||
*/
|
||||
#[Reactive]
|
||||
public ?float $longitude = null;
|
||||
|
||||
/**
|
||||
* The map zoom level (1-18)
|
||||
*/
|
||||
#[Reactive]
|
||||
public int $zoom = 13;
|
||||
|
||||
/**
|
||||
* Array of markers to display on the map
|
||||
*/
|
||||
#[Reactive]
|
||||
public array $markers = [];
|
||||
|
||||
/**
|
||||
* Currently selected location details
|
||||
*/
|
||||
#[Reactive]
|
||||
public ?array $selectedLocation = null;
|
||||
|
||||
/**
|
||||
* Whether the map is in interactive mode
|
||||
*/
|
||||
#[Reactive]
|
||||
public bool $interactive = true;
|
||||
|
||||
/**
|
||||
* Whether to show the map controls
|
||||
*/
|
||||
#[Reactive]
|
||||
public bool $showControls = true;
|
||||
|
||||
/**
|
||||
* Event listeners for the component
|
||||
*/
|
||||
protected $listeners = [
|
||||
'locationSelected' => 'handleLocationSelected',
|
||||
'markerClicked' => 'handleMarkerClicked',
|
||||
'mapMoved' => 'handleMapMoved',
|
||||
'zoomChanged' => 'handleZoomChanged'
|
||||
];
|
||||
|
||||
/**
|
||||
* Mount the component
|
||||
*/
|
||||
public function mount(
|
||||
?float $latitude = null,
|
||||
?float $longitude = null,
|
||||
?int $zoom = null,
|
||||
array $markers = [],
|
||||
bool $interactive = true,
|
||||
bool $showControls = true
|
||||
) {
|
||||
$this->latitude = $latitude;
|
||||
$this->longitude = $longitude;
|
||||
$this->zoom = $zoom ?? $this->zoom;
|
||||
$this->markers = $markers;
|
||||
$this->interactive = $interactive;
|
||||
$this->showControls = $showControls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle when a location is selected
|
||||
*/
|
||||
public function handleLocationSelected(array $location)
|
||||
{
|
||||
$this->selectedLocation = $location;
|
||||
$this->latitude = $location['latitude'];
|
||||
$this->longitude = $location['longitude'];
|
||||
|
||||
$this->dispatch('location-updated', [
|
||||
'latitude' => $this->latitude,
|
||||
'longitude' => $this->longitude,
|
||||
'location' => $location
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle when a marker is clicked
|
||||
*/
|
||||
public function handleMarkerClicked(array $marker)
|
||||
{
|
||||
$this->selectedLocation = $marker;
|
||||
|
||||
$this->dispatch('marker-selected', [
|
||||
'marker' => $marker
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle when the map is moved
|
||||
*/
|
||||
public function handleMapMoved(float $latitude, float $longitude)
|
||||
{
|
||||
$this->latitude = $latitude;
|
||||
$this->longitude = $longitude;
|
||||
|
||||
$this->dispatch('map-moved', [
|
||||
'latitude' => $latitude,
|
||||
'longitude' => $longitude
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle when the zoom level changes
|
||||
*/
|
||||
public function handleZoomChanged(int $zoom)
|
||||
{
|
||||
$this->zoom = $zoom;
|
||||
|
||||
$this->dispatch('zoom-changed', [
|
||||
'zoom' => $zoom
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the map configuration
|
||||
*/
|
||||
protected function getMapConfig(): array
|
||||
{
|
||||
return [
|
||||
'center' => [
|
||||
'lat' => $this->latitude,
|
||||
'lng' => $this->longitude,
|
||||
],
|
||||
'zoom' => $this->zoom,
|
||||
'markers' => $this->markers,
|
||||
'interactive' => $this->interactive,
|
||||
'showControls' => $this->showControls,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.location.location-map', [
|
||||
'mapConfig' => $this->getMapConfig()
|
||||
]);
|
||||
}
|
||||
}
|
||||
167
app/Livewire/Location/LocationSelectorComponent.php
Normal file
167
app/Livewire/Location/LocationSelectorComponent.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Location;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Services\GeocodeService;
|
||||
use App\Exceptions\GeocodingException;
|
||||
use App\Exceptions\ValidationException;
|
||||
|
||||
class LocationSelectorComponent extends Component
|
||||
{
|
||||
// Search State
|
||||
public string $searchQuery = '';
|
||||
public array $searchResults = [];
|
||||
public bool $isSearching = false;
|
||||
public ?string $validationError = null;
|
||||
|
||||
// Location State
|
||||
public ?float $latitude = null;
|
||||
public ?float $longitude = null;
|
||||
public ?string $formattedAddress = null;
|
||||
public bool $isValidLocation = false;
|
||||
|
||||
// UI State
|
||||
public bool $showSearchResults = false;
|
||||
public bool $isLoadingLocation = false;
|
||||
public string $mode = 'search'; // search|coordinates|current
|
||||
|
||||
// Component Configuration
|
||||
protected $geocodeService;
|
||||
|
||||
protected $listeners = [
|
||||
'mapLocationSelected' => 'handleMapLocation',
|
||||
'clearLocation' => 'clearSelection',
|
||||
];
|
||||
|
||||
public function mount(GeocodeService $geocodeService)
|
||||
{
|
||||
$this->geocodeService = $geocodeService;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.location.location-selector');
|
||||
}
|
||||
|
||||
public function updatedSearchQuery()
|
||||
{
|
||||
if (strlen($this->searchQuery) < 3) {
|
||||
$this->searchResults = [];
|
||||
$this->showSearchResults = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->isSearching = true;
|
||||
$this->searchResults = $this->geocodeService->geocode($this->searchQuery);
|
||||
$this->showSearchResults = true;
|
||||
$this->validationError = null;
|
||||
} catch (GeocodingException $e) {
|
||||
$this->validationError = $e->getMessage();
|
||||
} finally {
|
||||
$this->isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function selectLocation(array $location)
|
||||
{
|
||||
try {
|
||||
if (!isset($location['lat'], $location['lon'])) {
|
||||
throw new ValidationException('Invalid location data');
|
||||
}
|
||||
|
||||
$this->setCoordinates($location['lat'], $location['lon']);
|
||||
$this->formattedAddress = $location['display_name'] ?? null;
|
||||
$this->showSearchResults = false;
|
||||
$this->searchQuery = '';
|
||||
$this->isValidLocation = true;
|
||||
$this->validationError = null;
|
||||
|
||||
$this->dispatch('locationSelected', [
|
||||
'latitude' => $this->latitude,
|
||||
'longitude' => $this->longitude,
|
||||
'address' => $this->formattedAddress,
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
$this->validationError = $e->getMessage();
|
||||
$this->isValidLocation = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function setCoordinates(float $lat, float $lon)
|
||||
{
|
||||
try {
|
||||
if (!$this->geocodeService->validateCoordinates($lat, $lon)) {
|
||||
throw new ValidationException('Invalid coordinates');
|
||||
}
|
||||
|
||||
$this->latitude = $lat;
|
||||
$this->longitude = $lon;
|
||||
$this->isValidLocation = true;
|
||||
$this->validationError = null;
|
||||
|
||||
$this->dispatch('coordinatesChanged', [
|
||||
'latitude' => $this->latitude,
|
||||
'longitude' => $this->longitude,
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
$this->validationError = $e->getMessage();
|
||||
$this->isValidLocation = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function handleMapLocation($lat, $lon)
|
||||
{
|
||||
$this->setCoordinates($lat, $lon);
|
||||
|
||||
try {
|
||||
$address = $this->geocodeService->reverseGeocode($lat, $lon);
|
||||
$this->formattedAddress = $address['display_name'] ?? null;
|
||||
} catch (GeocodingException $e) {
|
||||
$this->validationError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function detectCurrentLocation()
|
||||
{
|
||||
$this->mode = 'current';
|
||||
$this->isLoadingLocation = true;
|
||||
$this->dispatch('requestCurrentLocation');
|
||||
}
|
||||
|
||||
public function clearSelection()
|
||||
{
|
||||
$this->reset([
|
||||
'latitude',
|
||||
'longitude',
|
||||
'formattedAddress',
|
||||
'isValidLocation',
|
||||
'searchQuery',
|
||||
'searchResults',
|
||||
'showSearchResults',
|
||||
'validationError',
|
||||
]);
|
||||
$this->mode = 'search';
|
||||
}
|
||||
|
||||
public function switchMode(string $mode)
|
||||
{
|
||||
if (!in_array($mode, ['search', 'coordinates', 'current'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->mode = $mode;
|
||||
$this->clearSelection();
|
||||
$this->dispatch('modeChanged', ['mode' => $mode]);
|
||||
}
|
||||
|
||||
protected function rules()
|
||||
{
|
||||
return [
|
||||
'latitude' => 'required|numeric|between:-90,90',
|
||||
'longitude' => 'required|numeric|between:-180,180',
|
||||
'searchQuery' => 'nullable|string|min:3|max:200',
|
||||
];
|
||||
}
|
||||
}
|
||||
362
app/Livewire/ManufacturersListingUniversal.php
Normal file
362
app/Livewire/ManufacturersListingUniversal.php
Normal file
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Manufacturer;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ManufacturersListingUniversal extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Universal Listing System Integration
|
||||
public string $entityType = 'manufacturers';
|
||||
|
||||
// Search and Filtering
|
||||
public string $search = '';
|
||||
public string $sortBy = 'name';
|
||||
public string $sortDirection = 'asc';
|
||||
public string $viewMode = 'grid';
|
||||
public int $perPage = 12;
|
||||
|
||||
// Manufacturer-specific filters
|
||||
public array $specializations = [];
|
||||
public array $totalRidesRange = [0, 1000];
|
||||
public array $industryPresenceRange = [0, 100];
|
||||
public array $foundedYearRange = [1800, 2025];
|
||||
public bool $activeOnly = false;
|
||||
public bool $innovationLeadersOnly = false;
|
||||
|
||||
// Performance optimization
|
||||
private string $cacheKeyPrefix = 'manufacturers_listing';
|
||||
private int $cacheProductPortfolioTtl = 21600; // 6 hours
|
||||
private int $cacheIndustryPresenceTtl = 43200; // 12 hours
|
||||
private int $cacheListingTtl = 1800; // 30 minutes
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'sortBy' => ['except' => 'name'],
|
||||
'sortDirection' => ['except' => 'asc'],
|
||||
'viewMode' => ['except' => 'grid'],
|
||||
'perPage' => ['except' => 12],
|
||||
'specializations' => ['except' => []],
|
||||
'totalRidesRange' => ['except' => [0, 1000]],
|
||||
'industryPresenceRange' => ['except' => [0, 100]],
|
||||
'foundedYearRange' => ['except' => [1800, 2025]],
|
||||
'activeOnly' => ['except' => false],
|
||||
'innovationLeadersOnly' => ['except' => false],
|
||||
'page' => ['except' => 1],
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
// Initialize filters with cached values for performance
|
||||
$this->initializeFilters();
|
||||
}
|
||||
|
||||
public function updatedSearch()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedSpecializations()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedTotalRidesRange()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedIndustryPresenceRange()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearRange()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedActiveOnly()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedInnovationLeadersOnly()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function clearFilters()
|
||||
{
|
||||
$this->reset([
|
||||
'search',
|
||||
'specializations',
|
||||
'totalRidesRange',
|
||||
'industryPresenceRange',
|
||||
'foundedYearRange',
|
||||
'activeOnly',
|
||||
'innovationLeadersOnly'
|
||||
]);
|
||||
$this->totalRidesRange = [0, 1000];
|
||||
$this->industryPresenceRange = [0, 100];
|
||||
$this->foundedYearRange = [1800, 2025];
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function setViewMode(string $mode)
|
||||
{
|
||||
$this->viewMode = $mode;
|
||||
}
|
||||
|
||||
public function setSortBy(string $field)
|
||||
{
|
||||
if ($this->sortBy === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $field;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$manufacturers = $this->getManufacturers();
|
||||
$statistics = $this->getStatistics();
|
||||
$productPortfolioData = $this->getProductPortfolioData();
|
||||
$industryPresenceData = $this->getIndustryPresenceData();
|
||||
|
||||
return view('livewire.manufacturers-listing-universal', [
|
||||
'manufacturers' => $manufacturers,
|
||||
'statistics' => $statistics,
|
||||
'productPortfolioData' => $productPortfolioData,
|
||||
'industryPresenceData' => $industryPresenceData,
|
||||
'hasActiveFilters' => $this->hasActiveFilters(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getManufacturers()
|
||||
{
|
||||
$cacheKey = $this->generateCacheKey();
|
||||
|
||||
return Cache::remember($cacheKey, $this->cacheListingTtl, function () {
|
||||
$query = Manufacturer::query()
|
||||
->select([
|
||||
'id', 'name', 'slug', 'headquarters', 'description', 'website',
|
||||
'total_rides', 'total_roller_coasters', 'founded_year',
|
||||
'industry_presence_score', 'specialization', 'is_active',
|
||||
'is_major_manufacturer', 'market_share_percentage', 'created_at'
|
||||
]);
|
||||
|
||||
// Apply search with Django parity algorithms
|
||||
if (!empty($this->search)) {
|
||||
$searchTerms = explode(' ', trim($this->search));
|
||||
$query->where(function (Builder $q) use ($searchTerms) {
|
||||
foreach ($searchTerms as $term) {
|
||||
$q->where(function (Builder $subQ) use ($term) {
|
||||
$subQ->where('name', 'ILIKE', "%{$term}%")
|
||||
->orWhere('description', 'ILIKE', "%{$term}%")
|
||||
->orWhere('headquarters', 'ILIKE', "%{$term}%")
|
||||
->orWhere('specialization', 'ILIKE', "%{$term}%");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply specialization filters
|
||||
if (!empty($this->specializations)) {
|
||||
$query->whereIn('specialization', $this->specializations);
|
||||
}
|
||||
|
||||
// Apply total rides range filter
|
||||
if ($this->totalRidesRange[0] > 0 || $this->totalRidesRange[1] < 1000) {
|
||||
$query->whereBetween('total_rides', $this->totalRidesRange);
|
||||
}
|
||||
|
||||
// Apply industry presence score range filter
|
||||
if ($this->industryPresenceRange[0] > 0 || $this->industryPresenceRange[1] < 100) {
|
||||
$query->whereBetween('industry_presence_score', $this->industryPresenceRange);
|
||||
}
|
||||
|
||||
// Apply founded year range filter
|
||||
if ($this->foundedYearRange[0] > 1800 || $this->foundedYearRange[1] < 2025) {
|
||||
$query->whereBetween('founded_year', $this->foundedYearRange);
|
||||
}
|
||||
|
||||
// Apply active filter
|
||||
if ($this->activeOnly) {
|
||||
$query->where('is_active', true);
|
||||
}
|
||||
|
||||
// Apply major manufacturers filter
|
||||
if ($this->innovationLeadersOnly) {
|
||||
$query->where('is_major_manufacturer', true);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
$query->orderBy($this->sortBy, $this->sortDirection);
|
||||
|
||||
// Add secondary sort for consistency
|
||||
if ($this->sortBy !== 'name') {
|
||||
$query->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
return $query->paginate($this->perPage);
|
||||
});
|
||||
}
|
||||
|
||||
private function getStatistics()
|
||||
{
|
||||
$cacheKey = "{$this->cacheKeyPrefix}_statistics";
|
||||
|
||||
return Cache::remember($cacheKey, $this->cacheListingTtl, function () {
|
||||
$baseQuery = Manufacturer::query();
|
||||
|
||||
// Apply same filters as main query for accurate statistics
|
||||
if (!empty($this->search)) {
|
||||
$searchTerms = explode(' ', trim($this->search));
|
||||
$baseQuery->where(function (Builder $q) use ($searchTerms) {
|
||||
foreach ($searchTerms as $term) {
|
||||
$q->where(function (Builder $subQ) use ($term) {
|
||||
$subQ->where('name', 'ILIKE', "%{$term}%")
|
||||
->orWhere('description', 'ILIKE', "%{$term}%")
|
||||
->orWhere('headquarters', 'ILIKE', "%{$term}%")
|
||||
->orWhere('specialization', 'ILIKE', "%{$term}%");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($this->specializations)) {
|
||||
$baseQuery->whereIn('specialization', $this->specializations);
|
||||
}
|
||||
|
||||
if ($this->totalRidesRange[0] > 0 || $this->totalRidesRange[1] < 1000) {
|
||||
$baseQuery->whereBetween('total_rides', $this->totalRidesRange);
|
||||
}
|
||||
|
||||
if ($this->industryPresenceRange[0] > 0 || $this->industryPresenceRange[1] < 100) {
|
||||
$baseQuery->whereBetween('industry_presence_score', $this->industryPresenceRange);
|
||||
}
|
||||
|
||||
if ($this->foundedYearRange[0] > 1800 || $this->foundedYearRange[1] < 2025) {
|
||||
$baseQuery->whereBetween('founded_year', $this->foundedYearRange);
|
||||
}
|
||||
|
||||
if ($this->activeOnly) {
|
||||
$baseQuery->where('is_active', true);
|
||||
}
|
||||
|
||||
if ($this->innovationLeadersOnly) {
|
||||
$baseQuery->where('is_major_manufacturer', true);
|
||||
}
|
||||
|
||||
return [
|
||||
'count' => $baseQuery->count(),
|
||||
'active_count' => (clone $baseQuery)->where('is_active', true)->count(),
|
||||
'total_rides_sum' => $baseQuery->sum('total_rides'),
|
||||
'avg_industry_presence' => round($baseQuery->avg('industry_presence_score'), 1),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function getProductPortfolioData()
|
||||
{
|
||||
$cacheKey = "{$this->cacheKeyPrefix}_product_portfolio";
|
||||
|
||||
return Cache::remember($cacheKey, $this->cacheProductPortfolioTtl, function () {
|
||||
return [
|
||||
'specialization_distribution' => Manufacturer::selectRaw('specialization, COUNT(*) as count')
|
||||
->groupBy('specialization')
|
||||
->pluck('count', 'specialization')
|
||||
->toArray(),
|
||||
'top_manufacturers_by_rides' => Manufacturer::orderBy('total_rides', 'desc')
|
||||
->limit(10)
|
||||
->pluck('total_rides', 'name')
|
||||
->toArray(),
|
||||
'major_manufacturers_count' => Manufacturer::where('is_major_manufacturer', true)->count(),
|
||||
'average_market_share' => round(Manufacturer::avg('market_share_percentage'), 2),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function getIndustryPresenceData()
|
||||
{
|
||||
$cacheKey = "{$this->cacheKeyPrefix}_industry_presence";
|
||||
|
||||
return Cache::remember($cacheKey, $this->cacheIndustryPresenceTtl, function () {
|
||||
return [
|
||||
'presence_score_ranges' => [
|
||||
'high' => Manufacturer::where('industry_presence_score', '>=', 80)->count(),
|
||||
'medium' => Manufacturer::whereBetween('industry_presence_score', [50, 79])->count(),
|
||||
'low' => Manufacturer::where('industry_presence_score', '<', 50)->count(),
|
||||
],
|
||||
'founding_decades' => Manufacturer::selectRaw('FLOOR(founded_year / 10) * 10 as decade, COUNT(*) as count')
|
||||
->groupBy('decade')
|
||||
->orderBy('decade')
|
||||
->pluck('count', 'decade')
|
||||
->toArray(),
|
||||
'active_vs_inactive' => [
|
||||
'active' => Manufacturer::where('is_active', true)->count(),
|
||||
'inactive' => Manufacturer::where('is_active', false)->count(),
|
||||
],
|
||||
'market_concentration' => Manufacturer::orderBy('market_share_percentage', 'desc')
|
||||
->limit(5)
|
||||
->pluck('market_share_percentage', 'name')
|
||||
->toArray(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function hasActiveFilters(): bool
|
||||
{
|
||||
return !empty($this->search) ||
|
||||
!empty($this->specializations) ||
|
||||
$this->totalRidesRange !== [0, 1000] ||
|
||||
$this->industryPresenceRange !== [0, 100] ||
|
||||
$this->foundedYearRange !== [1800, 2025] ||
|
||||
$this->activeOnly ||
|
||||
$this->innovationLeadersOnly;
|
||||
}
|
||||
|
||||
private function generateCacheKey(): string
|
||||
{
|
||||
$filterHash = md5(serialize([
|
||||
'search' => $this->search,
|
||||
'sortBy' => $this->sortBy,
|
||||
'sortDirection' => $this->sortDirection,
|
||||
'specializations' => $this->specializations,
|
||||
'totalRidesRange' => $this->totalRidesRange,
|
||||
'industryPresenceRange' => $this->industryPresenceRange,
|
||||
'foundedYearRange' => $this->foundedYearRange,
|
||||
'activeOnly' => $this->activeOnly,
|
||||
'innovationLeadersOnly' => $this->innovationLeadersOnly,
|
||||
'perPage' => $this->perPage,
|
||||
'page' => $this->getPage(),
|
||||
]));
|
||||
|
||||
return "{$this->cacheKeyPrefix}_{$filterHash}";
|
||||
}
|
||||
|
||||
private function initializeFilters()
|
||||
{
|
||||
// Initialize with sensible defaults for manufacturer filtering
|
||||
if (empty($this->totalRidesRange)) {
|
||||
$this->totalRidesRange = [0, 1000];
|
||||
}
|
||||
|
||||
if (empty($this->industryPresenceRange)) {
|
||||
$this->industryPresenceRange = [0, 100];
|
||||
}
|
||||
|
||||
if (empty($this->foundedYearRange)) {
|
||||
$this->foundedYearRange = [1800, 2025];
|
||||
}
|
||||
}
|
||||
}
|
||||
25
app/Livewire/MobileMenuComponent.php
Normal file
25
app/Livewire/MobileMenuComponent.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class MobileMenuComponent extends Component
|
||||
{
|
||||
public bool $isOpen = false;
|
||||
|
||||
public function toggle()
|
||||
{
|
||||
$this->isOpen = !$this->isOpen;
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$this->isOpen = false;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.mobile-menu-component');
|
||||
}
|
||||
}
|
||||
54
app/Livewire/OperatorHierarchyView.php
Normal file
54
app/Livewire/OperatorHierarchyView.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorHierarchyView extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operator-hierarchy-view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/Livewire/OperatorParksListing.php
Normal file
57
app/Livewire/OperatorParksListing.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorParksListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operator-parks-listing');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/OperatorPortfolioCard.php
Normal file
54
app/Livewire/OperatorPortfolioCard.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorPortfolioCard extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operator-portfolio-card');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/OperatorsIndustryStats.php
Normal file
54
app/Livewire/OperatorsIndustryStats.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorsIndustryStats extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operators-industry-stats');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
476
app/Livewire/OperatorsListing.php
Normal file
476
app/Livewire/OperatorsListing.php
Normal file
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Operator;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class OperatorsListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'roles')]
|
||||
public array $roleFilter = [];
|
||||
|
||||
#[Url(as: 'sector')]
|
||||
public string $industrySector = '';
|
||||
|
||||
#[Url(as: 'size')]
|
||||
public string $companySize = '';
|
||||
|
||||
#[Url(as: 'founded_from')]
|
||||
public string $foundedYearFrom = '';
|
||||
|
||||
#[Url(as: 'founded_to')]
|
||||
public string $foundedYearTo = '';
|
||||
|
||||
#[Url(as: 'presence')]
|
||||
public string $geographicPresence = '';
|
||||
|
||||
#[Url(as: 'min_revenue')]
|
||||
public string $minRevenue = '';
|
||||
|
||||
#[Url(as: 'max_revenue')]
|
||||
public string $maxRevenue = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'dir')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
#[Url(as: 'view')]
|
||||
public string $viewMode = 'grid';
|
||||
|
||||
public int $perPage = 20;
|
||||
public array $industryStats = [];
|
||||
public array $marketData = [];
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'roleFilter' => ['except' => []],
|
||||
'industrySector' => ['except' => ''],
|
||||
'companySize' => ['except' => ''],
|
||||
'foundedYearFrom' => ['except' => ''],
|
||||
'foundedYearTo' => ['except' => ''],
|
||||
'geographicPresence' => ['except' => ''],
|
||||
'minRevenue' => ['except' => ''],
|
||||
'maxRevenue' => ['except' => ''],
|
||||
'sortBy' => ['except' => 'name'],
|
||||
'sortDirection' => ['except' => 'asc'],
|
||||
'viewMode' => ['except' => 'grid'],
|
||||
'page' => ['except' => 1],
|
||||
];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadIndustryStatistics();
|
||||
$this->loadMarketData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load industry statistics with caching
|
||||
*/
|
||||
protected function loadIndustryStatistics(): void
|
||||
{
|
||||
$this->industryStats = Cache::remember(
|
||||
'operators.industry.stats',
|
||||
now()->addHours(6),
|
||||
fn() => $this->calculateIndustryStatistics()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load market analysis data with caching
|
||||
*/
|
||||
protected function loadMarketData(): void
|
||||
{
|
||||
$this->marketData = Cache::remember(
|
||||
'operators.market.data',
|
||||
now()->addHours(12),
|
||||
fn() => $this->loadMarketAnalysis()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate comprehensive industry statistics
|
||||
*/
|
||||
protected function calculateIndustryStatistics(): array
|
||||
{
|
||||
return [
|
||||
'total_operators' => Operator::active()->count(),
|
||||
'park_operators' => Operator::active()->parkOperators()->count(),
|
||||
'manufacturers' => Operator::active()->manufacturers()->count(),
|
||||
'designers' => Operator::active()->designers()->count(),
|
||||
'mixed_role' => Operator::active()
|
||||
->whereHas('parks')
|
||||
->whereHas('manufactured_rides')
|
||||
->count(),
|
||||
'sectors' => Operator::active()
|
||||
->select('industry_sector', DB::raw('count(*) as count'))
|
||||
->whereNotNull('industry_sector')
|
||||
->groupBy('industry_sector')
|
||||
->orderByDesc('count')
|
||||
->get()
|
||||
->pluck('count', 'industry_sector')
|
||||
->toArray(),
|
||||
'company_sizes' => [
|
||||
'small' => Operator::active()->companySize('small')->count(),
|
||||
'medium' => Operator::active()->companySize('medium')->count(),
|
||||
'large' => Operator::active()->companySize('large')->count(),
|
||||
'enterprise' => Operator::active()->companySize('enterprise')->count(),
|
||||
],
|
||||
'geographic_distribution' => Operator::active()
|
||||
->whereHas('parks.location')
|
||||
->with('parks.location')
|
||||
->get()
|
||||
->flatMap(fn($op) => $op->parks->pluck('location.country'))
|
||||
->countBy()
|
||||
->sortDesc()
|
||||
->take(10)
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load market analysis data
|
||||
*/
|
||||
protected function loadMarketAnalysis(): array
|
||||
{
|
||||
return [
|
||||
'total_market_cap' => Operator::active()
|
||||
->whereNotNull('market_cap')
|
||||
->sum('market_cap'),
|
||||
'total_revenue' => Operator::active()
|
||||
->whereNotNull('annual_revenue')
|
||||
->sum('annual_revenue'),
|
||||
'average_parks_per_operator' => Operator::active()
|
||||
->parkOperators()
|
||||
->avg('total_parks'),
|
||||
'top_operators_by_parks' => Operator::active()
|
||||
->parkOperators()
|
||||
->orderByDesc('total_parks')
|
||||
->take(5)
|
||||
->get(['name', 'total_parks'])
|
||||
->toArray(),
|
||||
'top_manufacturers_by_rides' => Operator::active()
|
||||
->manufacturers()
|
||||
->orderByDesc('total_rides_manufactured')
|
||||
->take(5)
|
||||
->get(['name', 'total_rides_manufactured'])
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Django parity dual-role search functionality
|
||||
*/
|
||||
public function dualRoleSearch($query, $roles = [])
|
||||
{
|
||||
return Operator::query()
|
||||
->when($query, function ($q) use ($query) {
|
||||
$terms = explode(' ', trim($query));
|
||||
foreach ($terms as $term) {
|
||||
if (strlen($term) >= 2) {
|
||||
$q->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhere('industry_sector', 'ilike', "%{$term}%")
|
||||
->orWhere('headquarters_location', 'ilike', "%{$term}%")
|
||||
->orWhereHas('location', function($locQuery) use ($term) {
|
||||
$locQuery->where('city', 'ilike', "%{$term}%")
|
||||
->orWhere('state', 'ilike', "%{$term}%")
|
||||
->orWhere('country', 'ilike', "%{$term}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
->when($roles, function ($q) use ($roles) {
|
||||
$q->where(function ($roleQuery) use ($roles) {
|
||||
if (in_array('park_operator', $roles)) {
|
||||
$roleQuery->whereHas('parks');
|
||||
}
|
||||
if (in_array('ride_manufacturer', $roles)) {
|
||||
$roleQuery->orWhereHas('manufactured_rides');
|
||||
}
|
||||
if (in_array('ride_designer', $roles)) {
|
||||
$roleQuery->orWhereHas('designed_rides');
|
||||
}
|
||||
});
|
||||
})
|
||||
->active()
|
||||
->with(['location', 'parks:id,operator_id,name', 'manufactured_rides:id,manufacturer_id,name', 'designed_rides:id,designer_id,name'])
|
||||
->withCount(['parks', 'manufactured_rides', 'designed_rides']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply advanced industry filters
|
||||
*/
|
||||
public function applyIndustryFilters($query)
|
||||
{
|
||||
return $query
|
||||
->when($this->industrySector, fn($q, $sector) =>
|
||||
$q->where('industry_sector', $sector))
|
||||
->when($this->companySize, fn($q, $size) =>
|
||||
$q->companySize($size))
|
||||
->when($this->foundedYearFrom, fn($q, $year) =>
|
||||
$q->where('founded_year', '>=', $year))
|
||||
->when($this->foundedYearTo, fn($q, $year) =>
|
||||
$q->where('founded_year', '<=', $year))
|
||||
->when($this->geographicPresence, function ($q, $presence) {
|
||||
switch ($presence) {
|
||||
case 'regional':
|
||||
$q->whereHas('parks', function ($parkQ) {
|
||||
$parkQ->whereHas('location', function ($locQ) {
|
||||
$locQ->havingRaw('COUNT(DISTINCT country) = 1');
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'international':
|
||||
$q->whereHas('parks', function ($parkQ) {
|
||||
$parkQ->whereHas('location', function ($locQ) {
|
||||
$locQ->havingRaw('COUNT(DISTINCT country) > 1');
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
})
|
||||
->when($this->minRevenue, fn($q, $revenue) =>
|
||||
$q->where('annual_revenue', '>=', $revenue))
|
||||
->when($this->maxRevenue, fn($q, $revenue) =>
|
||||
$q->where('annual_revenue', '<=', $revenue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operators with optimized caching
|
||||
*/
|
||||
public function getOperatorsProperty()
|
||||
{
|
||||
$cacheKey = "operators.listing." . md5(serialize([
|
||||
'search' => $this->search,
|
||||
'roleFilter' => $this->roleFilter,
|
||||
'industrySector' => $this->industrySector,
|
||||
'companySize' => $this->companySize,
|
||||
'foundedYearFrom' => $this->foundedYearFrom,
|
||||
'foundedYearTo' => $this->foundedYearTo,
|
||||
'geographicPresence' => $this->geographicPresence,
|
||||
'minRevenue' => $this->minRevenue,
|
||||
'maxRevenue' => $this->maxRevenue,
|
||||
'sortBy' => $this->sortBy,
|
||||
'sortDirection' => $this->sortDirection,
|
||||
'page' => $this->getPage(),
|
||||
'perPage' => $this->perPage,
|
||||
]));
|
||||
|
||||
return Cache::remember($cacheKey, now()->addMinutes(30), function() {
|
||||
$query = $this->dualRoleSearch($this->search, $this->roleFilter);
|
||||
$query = $this->applyIndustryFilters($query);
|
||||
|
||||
// Apply sorting
|
||||
switch ($this->sortBy) {
|
||||
case 'name':
|
||||
$query->orderBy('name', $this->sortDirection);
|
||||
break;
|
||||
case 'founded_year':
|
||||
$query->orderBy('founded_year', $this->sortDirection);
|
||||
break;
|
||||
case 'parks_count':
|
||||
$query->orderBy('total_parks', $this->sortDirection);
|
||||
break;
|
||||
case 'rides_count':
|
||||
$query->orderBy('total_rides_manufactured', $this->sortDirection);
|
||||
break;
|
||||
case 'revenue':
|
||||
$query->orderBy('annual_revenue', $this->sortDirection);
|
||||
break;
|
||||
case 'market_influence':
|
||||
$query->orderByRaw('(total_parks * 10 + total_rides_manufactured * 2) ' . $this->sortDirection);
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
return $query->paginate($this->perPage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search and reset pagination
|
||||
*/
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role filter and reset pagination
|
||||
*/
|
||||
public function updatedRoleFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any filter and reset pagination
|
||||
*/
|
||||
public function updatedIndustrySector(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedCompanySize(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearFrom(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearTo(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedGeographicPresence(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMinRevenue(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMaxRevenue(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by specific column
|
||||
*/
|
||||
public function sortBy(string $column): void
|
||||
{
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change view mode
|
||||
*/
|
||||
public function setViewMode(string $mode): void
|
||||
{
|
||||
$this->viewMode = $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->search = '';
|
||||
$this->roleFilter = [];
|
||||
$this->industrySector = '';
|
||||
$this->companySize = '';
|
||||
$this->foundedYearFrom = '';
|
||||
$this->foundedYearTo = '';
|
||||
$this->geographicPresence = '';
|
||||
$this->minRevenue = '';
|
||||
$this->maxRevenue = '';
|
||||
$this->sortBy = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle role filter
|
||||
*/
|
||||
public function toggleRoleFilter(string $role): void
|
||||
{
|
||||
if (in_array($role, $this->roleFilter)) {
|
||||
$this->roleFilter = array_values(array_diff($this->roleFilter, [$role]));
|
||||
} else {
|
||||
$this->roleFilter[] = $role;
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(): void
|
||||
{
|
||||
Cache::forget('operators.industry.stats');
|
||||
Cache::forget('operators.market.data');
|
||||
|
||||
// Clear listing cache pattern - simplified approach
|
||||
$cacheKeys = [
|
||||
'operators.listing.*'
|
||||
];
|
||||
|
||||
foreach ($cacheKeys as $pattern) {
|
||||
Cache::forget($pattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operators-listing', [
|
||||
'operators' => $this->operators,
|
||||
'industryStats' => $this->industryStats,
|
||||
'marketData' => $this->marketData,
|
||||
]);
|
||||
}
|
||||
}
|
||||
479
app/Livewire/OperatorsListingUniversal.php
Normal file
479
app/Livewire/OperatorsListingUniversal.php
Normal file
@@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Operator;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class OperatorsListingUniversal extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Universal Listing System Integration
|
||||
public string $entityType = 'operators';
|
||||
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'roles')]
|
||||
public array $roleFilter = [];
|
||||
|
||||
#[Url(as: 'sector')]
|
||||
public string $industrySector = '';
|
||||
|
||||
#[Url(as: 'size')]
|
||||
public string $companySize = '';
|
||||
|
||||
#[Url(as: 'founded_from')]
|
||||
public string $foundedYearFrom = '';
|
||||
|
||||
#[Url(as: 'founded_to')]
|
||||
public string $foundedYearTo = '';
|
||||
|
||||
#[Url(as: 'presence')]
|
||||
public string $geographicPresence = '';
|
||||
|
||||
#[Url(as: 'min_revenue')]
|
||||
public string $minRevenue = '';
|
||||
|
||||
#[Url(as: 'max_revenue')]
|
||||
public string $maxRevenue = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'dir')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
#[Url(as: 'view')]
|
||||
public string $viewMode = 'grid';
|
||||
|
||||
public int $perPage = 20;
|
||||
public array $industryStats = [];
|
||||
public array $marketData = [];
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'roleFilter' => ['except' => []],
|
||||
'industrySector' => ['except' => ''],
|
||||
'companySize' => ['except' => ''],
|
||||
'foundedYearFrom' => ['except' => ''],
|
||||
'foundedYearTo' => ['except' => ''],
|
||||
'geographicPresence' => ['except' => ''],
|
||||
'minRevenue' => ['except' => ''],
|
||||
'maxRevenue' => ['except' => ''],
|
||||
'sortBy' => ['except' => 'name'],
|
||||
'sortDirection' => ['except' => 'asc'],
|
||||
'viewMode' => ['except' => 'grid'],
|
||||
'page' => ['except' => 1],
|
||||
];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadIndustryStatistics();
|
||||
$this->loadMarketData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load industry statistics with caching
|
||||
*/
|
||||
protected function loadIndustryStatistics(): void
|
||||
{
|
||||
$this->industryStats = Cache::remember(
|
||||
'operators.industry.stats',
|
||||
now()->addHours(6),
|
||||
fn() => $this->calculateIndustryStatistics()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load market analysis data with caching
|
||||
*/
|
||||
protected function loadMarketData(): void
|
||||
{
|
||||
$this->marketData = Cache::remember(
|
||||
'operators.market.data',
|
||||
now()->addHours(12),
|
||||
fn() => $this->loadMarketAnalysis()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate comprehensive industry statistics
|
||||
*/
|
||||
protected function calculateIndustryStatistics(): array
|
||||
{
|
||||
return [
|
||||
'total_operators' => Operator::active()->count(),
|
||||
'park_operators' => Operator::active()->parkOperators()->count(),
|
||||
'manufacturers' => Operator::active()->manufacturers()->count(),
|
||||
'designers' => Operator::active()->designers()->count(),
|
||||
'mixed_role' => Operator::active()
|
||||
->whereHas('parks')
|
||||
->whereHas('manufactured_rides')
|
||||
->count(),
|
||||
'sectors' => Operator::active()
|
||||
->select('industry_sector', DB::raw('count(*) as count'))
|
||||
->whereNotNull('industry_sector')
|
||||
->groupBy('industry_sector')
|
||||
->orderByDesc('count')
|
||||
->get()
|
||||
->pluck('count', 'industry_sector')
|
||||
->toArray(),
|
||||
'company_sizes' => [
|
||||
'small' => Operator::active()->companySize('small')->count(),
|
||||
'medium' => Operator::active()->companySize('medium')->count(),
|
||||
'large' => Operator::active()->companySize('large')->count(),
|
||||
'enterprise' => Operator::active()->companySize('enterprise')->count(),
|
||||
],
|
||||
'geographic_distribution' => Operator::active()
|
||||
->whereHas('parks.location')
|
||||
->with('parks.location')
|
||||
->get()
|
||||
->flatMap(fn($op) => $op->parks->pluck('location.country'))
|
||||
->countBy()
|
||||
->sortDesc()
|
||||
->take(10)
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load market analysis data
|
||||
*/
|
||||
protected function loadMarketAnalysis(): array
|
||||
{
|
||||
return [
|
||||
'total_market_cap' => Operator::active()
|
||||
->whereNotNull('market_cap')
|
||||
->sum('market_cap'),
|
||||
'total_revenue' => Operator::active()
|
||||
->whereNotNull('annual_revenue')
|
||||
->sum('annual_revenue'),
|
||||
'average_parks_per_operator' => Operator::active()
|
||||
->parkOperators()
|
||||
->avg('total_parks'),
|
||||
'top_operators_by_parks' => Operator::active()
|
||||
->parkOperators()
|
||||
->orderByDesc('total_parks')
|
||||
->take(5)
|
||||
->get(['name', 'total_parks'])
|
||||
->toArray(),
|
||||
'top_manufacturers_by_rides' => Operator::active()
|
||||
->manufacturers()
|
||||
->orderByDesc('total_rides_manufactured')
|
||||
->take(5)
|
||||
->get(['name', 'total_rides_manufactured'])
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Django parity dual-role search functionality
|
||||
*/
|
||||
public function dualRoleSearch($query, $roles = [])
|
||||
{
|
||||
return Operator::query()
|
||||
->when($query, function ($q) use ($query) {
|
||||
$terms = explode(' ', trim($query));
|
||||
foreach ($terms as $term) {
|
||||
if (strlen($term) >= 2) {
|
||||
$q->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhere('industry_sector', 'ilike', "%{$term}%")
|
||||
->orWhere('headquarters_location', 'ilike', "%{$term}%")
|
||||
->orWhereHas('location', function($locQuery) use ($term) {
|
||||
$locQuery->where('city', 'ilike', "%{$term}%")
|
||||
->orWhere('state', 'ilike', "%{$term}%")
|
||||
->orWhere('country', 'ilike', "%{$term}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
->when($roles, function ($q) use ($roles) {
|
||||
$q->where(function ($roleQuery) use ($roles) {
|
||||
if (in_array('park_operator', $roles)) {
|
||||
$roleQuery->whereHas('parks');
|
||||
}
|
||||
if (in_array('ride_manufacturer', $roles)) {
|
||||
$roleQuery->orWhereHas('manufactured_rides');
|
||||
}
|
||||
if (in_array('ride_designer', $roles)) {
|
||||
$roleQuery->orWhereHas('designed_rides');
|
||||
}
|
||||
});
|
||||
})
|
||||
->active()
|
||||
->with(['location', 'parks:id,operator_id,name', 'manufactured_rides:id,manufacturer_id,name', 'designed_rides:id,designer_id,name'])
|
||||
->withCount(['parks', 'manufactured_rides', 'designed_rides']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply advanced industry filters
|
||||
*/
|
||||
public function applyIndustryFilters($query)
|
||||
{
|
||||
return $query
|
||||
->when($this->industrySector, fn($q, $sector) =>
|
||||
$q->where('industry_sector', $sector))
|
||||
->when($this->companySize, fn($q, $size) =>
|
||||
$q->companySize($size))
|
||||
->when($this->foundedYearFrom, fn($q, $year) =>
|
||||
$q->where('founded_year', '>=', $year))
|
||||
->when($this->foundedYearTo, fn($q, $year) =>
|
||||
$q->where('founded_year', '<=', $year))
|
||||
->when($this->geographicPresence, function ($q, $presence) {
|
||||
switch ($presence) {
|
||||
case 'regional':
|
||||
$q->whereHas('parks', function ($parkQ) {
|
||||
$parkQ->whereHas('location', function ($locQ) {
|
||||
$locQ->havingRaw('COUNT(DISTINCT country) = 1');
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'international':
|
||||
$q->whereHas('parks', function ($parkQ) {
|
||||
$parkQ->whereHas('location', function ($locQ) {
|
||||
$locQ->havingRaw('COUNT(DISTINCT country) > 1');
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
})
|
||||
->when($this->minRevenue, fn($q, $revenue) =>
|
||||
$q->where('annual_revenue', '>=', $revenue))
|
||||
->when($this->maxRevenue, fn($q, $revenue) =>
|
||||
$q->where('annual_revenue', '<=', $revenue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get operators with optimized caching
|
||||
*/
|
||||
public function getOperatorsProperty()
|
||||
{
|
||||
$cacheKey = "operators.listing." . md5(serialize([
|
||||
'search' => $this->search,
|
||||
'roleFilter' => $this->roleFilter,
|
||||
'industrySector' => $this->industrySector,
|
||||
'companySize' => $this->companySize,
|
||||
'foundedYearFrom' => $this->foundedYearFrom,
|
||||
'foundedYearTo' => $this->foundedYearTo,
|
||||
'geographicPresence' => $this->geographicPresence,
|
||||
'minRevenue' => $this->minRevenue,
|
||||
'maxRevenue' => $this->maxRevenue,
|
||||
'sortBy' => $this->sortBy,
|
||||
'sortDirection' => $this->sortDirection,
|
||||
'page' => $this->getPage(),
|
||||
'perPage' => $this->perPage,
|
||||
]));
|
||||
|
||||
return Cache::remember($cacheKey, now()->addMinutes(30), function() {
|
||||
$query = $this->dualRoleSearch($this->search, $this->roleFilter);
|
||||
$query = $this->applyIndustryFilters($query);
|
||||
|
||||
// Apply sorting
|
||||
switch ($this->sortBy) {
|
||||
case 'name':
|
||||
$query->orderBy('name', $this->sortDirection);
|
||||
break;
|
||||
case 'founded_year':
|
||||
$query->orderBy('founded_year', $this->sortDirection);
|
||||
break;
|
||||
case 'parks_count':
|
||||
$query->orderBy('total_parks', $this->sortDirection);
|
||||
break;
|
||||
case 'rides_count':
|
||||
$query->orderBy('total_rides_manufactured', $this->sortDirection);
|
||||
break;
|
||||
case 'revenue':
|
||||
$query->orderBy('annual_revenue', $this->sortDirection);
|
||||
break;
|
||||
case 'market_influence':
|
||||
$query->orderByRaw('(total_parks * 10 + total_rides_manufactured * 2) ' . $this->sortDirection);
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
return $query->paginate($this->perPage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search and reset pagination
|
||||
*/
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update role filter and reset pagination
|
||||
*/
|
||||
public function updatedRoleFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any filter and reset pagination
|
||||
*/
|
||||
public function updatedIndustrySector(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedCompanySize(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearFrom(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedFoundedYearTo(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedGeographicPresence(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMinRevenue(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
public function updatedMaxRevenue(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort by specific column
|
||||
*/
|
||||
public function sortBy(string $column): void
|
||||
{
|
||||
if ($this->sortBy === $column) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $column;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change view mode
|
||||
*/
|
||||
public function setViewMode(string $mode): void
|
||||
{
|
||||
$this->viewMode = $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->search = '';
|
||||
$this->roleFilter = [];
|
||||
$this->industrySector = '';
|
||||
$this->companySize = '';
|
||||
$this->foundedYearFrom = '';
|
||||
$this->foundedYearTo = '';
|
||||
$this->geographicPresence = '';
|
||||
$this->minRevenue = '';
|
||||
$this->maxRevenue = '';
|
||||
$this->sortBy = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle role filter
|
||||
*/
|
||||
public function toggleRoleFilter(string $role): void
|
||||
{
|
||||
if (in_array($role, $this->roleFilter)) {
|
||||
$this->roleFilter = array_values(array_diff($this->roleFilter, [$role]));
|
||||
} else {
|
||||
$this->roleFilter[] = $role;
|
||||
}
|
||||
|
||||
$this->resetPage();
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(): void
|
||||
{
|
||||
Cache::forget('operators.industry.stats');
|
||||
Cache::forget('operators.market.data');
|
||||
|
||||
// Clear listing cache pattern - simplified approach
|
||||
$cacheKeys = [
|
||||
'operators.listing.*'
|
||||
];
|
||||
|
||||
foreach ($cacheKeys as $pattern) {
|
||||
Cache::forget($pattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operators-listing-universal', [
|
||||
'operators' => $this->operators,
|
||||
'industryStats' => $this->industryStats,
|
||||
'marketData' => $this->marketData,
|
||||
]);
|
||||
}
|
||||
}
|
||||
54
app/Livewire/OperatorsMarketAnalysis.php
Normal file
54
app/Livewire/OperatorsMarketAnalysis.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorsMarketAnalysis extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operators-market-analysis');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/OperatorsRoleFilter.php
Normal file
54
app/Livewire/OperatorsRoleFilter.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class OperatorsRoleFilter extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.operators-role-filter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use App\Models\Park;
|
||||
use App\Enums\ParkStatus;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Contracts\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ParkListComponent extends Component
|
||||
{
|
||||
@@ -17,6 +17,7 @@ class ParkListComponent extends Component
|
||||
public string $sort = 'name';
|
||||
public string $direction = 'asc';
|
||||
public ?string $operator = null;
|
||||
public string $viewMode = 'grid';
|
||||
|
||||
/** @var array<string, string> */
|
||||
public array $sortOptions = [
|
||||
@@ -33,6 +34,7 @@ class ParkListComponent extends Component
|
||||
'sort' => ['except' => 'name'],
|
||||
'direction' => ['except' => 'asc'],
|
||||
'operator' => ['except' => ''],
|
||||
'viewMode' => ['except' => 'grid'],
|
||||
];
|
||||
|
||||
public function mount(): void
|
||||
@@ -55,6 +57,11 @@ class ParkListComponent extends Component
|
||||
$this->resetPage('parks-page');
|
||||
}
|
||||
|
||||
public function updatedViewMode(): void
|
||||
{
|
||||
// No need to reset page when changing view mode
|
||||
}
|
||||
|
||||
public function sortBy(string $field): void
|
||||
{
|
||||
if ($this->sort === $field) {
|
||||
@@ -84,10 +91,12 @@ class ParkListComponent extends Component
|
||||
public function render()
|
||||
{
|
||||
$query = Park::query()
|
||||
->with(['operator'])
|
||||
->with(['operator', 'location'])
|
||||
->when($this->search, function (Builder $query) {
|
||||
$query->where('name', 'like', '%' . $this->search . '%')
|
||||
$query->where(function (Builder $q) {
|
||||
$q->where('name', 'like', '%' . $this->search . '%')
|
||||
->orWhere('description', 'like', '%' . $this->search . '%');
|
||||
});
|
||||
})
|
||||
->when($this->status, function (Builder $query) {
|
||||
$query->where('status', $this->status);
|
||||
@@ -119,6 +128,7 @@ class ParkListComponent extends Component
|
||||
'parks' => $query->paginate(12, pageName: 'parks-page'),
|
||||
'statusOptions' => $this->getStatusOptions(),
|
||||
'operatorOptions' => $this->getOperatorOptions(),
|
||||
'viewMode' => $this->viewMode,
|
||||
]);
|
||||
}
|
||||
}
|
||||
372
app/Livewire/ParkRidesListing.php
Normal file
372
app/Livewire/ParkRidesListing.php
Normal file
@@ -0,0 +1,372 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Models\Park;
|
||||
use App\Enums\RideCategory;
|
||||
use App\Enums\RideStatus;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ParkRidesListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Required park context
|
||||
public Park $park;
|
||||
|
||||
// URL-bound search and filter properties
|
||||
#[Url(as: 'search')]
|
||||
public string $searchTerm = '';
|
||||
|
||||
#[Url(as: 'category')]
|
||||
public ?string $selectedCategory = null;
|
||||
|
||||
#[Url(as: 'status')]
|
||||
public ?string $selectedStatus = null;
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'direction')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
// UI state
|
||||
public bool $showFilters = false;
|
||||
public int $perPage = 12;
|
||||
|
||||
// Cached data
|
||||
public array $categories = [];
|
||||
public array $statuses = [];
|
||||
public array $sortOptions = [];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(Park $park): void
|
||||
{
|
||||
$this->park = $park;
|
||||
$this->loadFilterOptions();
|
||||
$this->setupSortOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load filter options specific to this park
|
||||
*/
|
||||
protected function loadFilterOptions(): void
|
||||
{
|
||||
$cacheKey = "park_rides_filters_{$this->park->id}";
|
||||
|
||||
$filterData = Cache::remember($cacheKey, 3600, function() {
|
||||
// Categories available in this park
|
||||
$categories = $this->park->rides()
|
||||
->select('category')
|
||||
->groupBy('category')
|
||||
->get()
|
||||
->map(function($ride) {
|
||||
$category = RideCategory::from($ride->category);
|
||||
return [
|
||||
'value' => $category->value,
|
||||
'label' => $category->name,
|
||||
'count' => $this->park->rides()->where('category', $category->value)->count()
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
// Statuses available in this park
|
||||
$statuses = $this->park->rides()
|
||||
->select('status')
|
||||
->groupBy('status')
|
||||
->get()
|
||||
->map(function($ride) {
|
||||
$status = RideStatus::from($ride->status);
|
||||
return [
|
||||
'value' => $status->value,
|
||||
'label' => $status->name,
|
||||
'count' => $this->park->rides()->where('status', $status->value)->count()
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
return compact('categories', 'statuses');
|
||||
});
|
||||
|
||||
$this->categories = $filterData['categories'];
|
||||
$this->statuses = $filterData['statuses'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup sort options
|
||||
*/
|
||||
protected function setupSortOptions(): void
|
||||
{
|
||||
$this->sortOptions = [
|
||||
'name' => 'Name',
|
||||
'opening_year' => 'Opening Year',
|
||||
'height_requirement' => 'Height Requirement',
|
||||
'created_at' => 'Date Added',
|
||||
'updated_at' => 'Last Updated'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search term and reset pagination
|
||||
*/
|
||||
public function updatedSearchTerm(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update category filter
|
||||
*/
|
||||
public function updatedSelectedCategory(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status filter
|
||||
*/
|
||||
public function updatedSelectedStatus(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sort options
|
||||
*/
|
||||
public function updatedSortBy(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sort direction
|
||||
*/
|
||||
public function updatedSortDirection(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set category filter
|
||||
*/
|
||||
public function setCategory(?string $category): void
|
||||
{
|
||||
$this->selectedCategory = $category === $this->selectedCategory ? null : $category;
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set status filter
|
||||
*/
|
||||
public function setStatus(?string $status): void
|
||||
{
|
||||
$this->selectedStatus = $status === $this->selectedStatus ? null : $status;
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sort parameters
|
||||
*/
|
||||
public function setSortBy(string $field): void
|
||||
{
|
||||
if ($this->sortBy === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $field;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle filters visibility
|
||||
*/
|
||||
public function toggleFilters(): void
|
||||
{
|
||||
$this->showFilters = !$this->showFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->searchTerm = '';
|
||||
$this->selectedCategory = null;
|
||||
$this->selectedStatus = null;
|
||||
$this->sortBy = 'name';
|
||||
$this->sortDirection = 'asc';
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered and sorted rides for this park
|
||||
*/
|
||||
public function getRidesProperty()
|
||||
{
|
||||
$cacheKey = $this->getCacheKey();
|
||||
|
||||
return Cache::remember($cacheKey, 300, function() {
|
||||
$query = $this->park->rides()
|
||||
->with(['manufacturer', 'designer', 'photos'])
|
||||
->when($this->searchTerm, function (Builder $query) {
|
||||
$query->where(function (Builder $subQuery) {
|
||||
$subQuery->where('name', 'ILIKE', "%{$this->searchTerm}%")
|
||||
->orWhere('description', 'ILIKE', "%{$this->searchTerm}%")
|
||||
->orWhereHas('manufacturer', function (Builder $manufacturerQuery) {
|
||||
$manufacturerQuery->where('name', 'ILIKE', "%{$this->searchTerm}%");
|
||||
})
|
||||
->orWhereHas('designer', function (Builder $designerQuery) {
|
||||
$designerQuery->where('name', 'ILIKE', "%{$this->searchTerm}%");
|
||||
});
|
||||
});
|
||||
})
|
||||
->when($this->selectedCategory, function (Builder $query) {
|
||||
$query->where('category', $this->selectedCategory);
|
||||
})
|
||||
->when($this->selectedStatus, function (Builder $query) {
|
||||
$query->where('status', $this->selectedStatus);
|
||||
});
|
||||
|
||||
// Apply sorting
|
||||
switch ($this->sortBy) {
|
||||
case 'name':
|
||||
$query->orderBy('name', $this->sortDirection);
|
||||
break;
|
||||
case 'opening_year':
|
||||
$query->orderBy('opening_year', $this->sortDirection)
|
||||
->orderBy('name', 'asc');
|
||||
break;
|
||||
case 'height_requirement':
|
||||
$query->orderBy('height_requirement', $this->sortDirection)
|
||||
->orderBy('name', 'asc');
|
||||
break;
|
||||
case 'created_at':
|
||||
$query->orderBy('created_at', $this->sortDirection);
|
||||
break;
|
||||
case 'updated_at':
|
||||
$query->orderBy('updated_at', $this->sortDirection);
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
return $query->paginate($this->perPage);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get park statistics
|
||||
*/
|
||||
public function getParkStatsProperty(): array
|
||||
{
|
||||
$cacheKey = "park_stats_{$this->park->id}";
|
||||
|
||||
return Cache::remember($cacheKey, 3600, function() {
|
||||
$totalRides = $this->park->rides()->count();
|
||||
$operatingRides = $this->park->rides()->where('status', 'operating')->count();
|
||||
$categories = $this->park->rides()
|
||||
->select('category')
|
||||
->groupBy('category')
|
||||
->get()
|
||||
->count();
|
||||
|
||||
$avgRating = $this->park->rides()
|
||||
->whereHas('reviews')
|
||||
->withAvg('reviews', 'rating')
|
||||
->get()
|
||||
->avg('reviews_avg_rating');
|
||||
|
||||
return [
|
||||
'total_rides' => $totalRides,
|
||||
'operating_rides' => $operatingRides,
|
||||
'categories' => $categories,
|
||||
'avg_rating' => $avgRating ? round($avgRating, 1) : null
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active filters count
|
||||
*/
|
||||
public function getActiveFiltersCountProperty(): int
|
||||
{
|
||||
return collect([
|
||||
$this->searchTerm,
|
||||
$this->selectedCategory,
|
||||
$this->selectedStatus
|
||||
])->filter()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for current state
|
||||
*/
|
||||
protected function getCacheKey(): string
|
||||
{
|
||||
return sprintf(
|
||||
'park_rides_%d_%s_%s_%s_%s_%s_%d_%d',
|
||||
$this->park->id,
|
||||
md5($this->searchTerm),
|
||||
$this->selectedCategory ?? 'all',
|
||||
$this->selectedStatus ?? 'all',
|
||||
$this->sortBy,
|
||||
$this->sortDirection,
|
||||
$this->perPage,
|
||||
$this->getPage()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.park-rides-listing', [
|
||||
'rides' => $this->rides,
|
||||
'parkStats' => $this->parkStats,
|
||||
'activeFiltersCount' => $this->activeFiltersCount
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset pagination when filters change
|
||||
*/
|
||||
public function resetPage($pageName = 'page'): void
|
||||
{
|
||||
$this->resetPage($pageName);
|
||||
|
||||
// Clear cache when filters change
|
||||
$this->clearComponentCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear component-specific cache
|
||||
*/
|
||||
protected function clearComponentCache(): void
|
||||
{
|
||||
$patterns = [
|
||||
"park_rides_{$this->park->id}_*",
|
||||
"park_stats_{$this->park->id}",
|
||||
"park_rides_filters_{$this->park->id}"
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
Cache::forget($pattern);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pagination view
|
||||
*/
|
||||
public function paginationView(): string
|
||||
{
|
||||
return 'livewire.pagination-links';
|
||||
}
|
||||
}
|
||||
54
app/Livewire/ParksFilters.php
Normal file
54
app/Livewire/ParksFilters.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ParksFilters extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.parks-filters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
476
app/Livewire/ParksListing.php
Normal file
476
app/Livewire/ParksListing.php
Normal file
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Operator;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ParksListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Search and Filter Properties with URL binding
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'operator')]
|
||||
public string $operatorId = '';
|
||||
|
||||
#[Url(as: 'region')]
|
||||
public string $region = '';
|
||||
|
||||
#[Url(as: 'country')]
|
||||
public string $country = '';
|
||||
|
||||
#[Url(as: 'type')]
|
||||
public string $parkType = '';
|
||||
|
||||
#[Url(as: 'year_from')]
|
||||
public string $openingYearFrom = '';
|
||||
|
||||
#[Url(as: 'year_to')]
|
||||
public string $openingYearTo = '';
|
||||
|
||||
#[Url(as: 'min_area')]
|
||||
public string $minArea = '';
|
||||
|
||||
#[Url(as: 'max_area')]
|
||||
public string $maxArea = '';
|
||||
|
||||
#[Url(as: 'min_rides')]
|
||||
public string $minRides = '';
|
||||
|
||||
#[Url(as: 'max_distance')]
|
||||
public string $maxDistance = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'dir')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
// Location Properties
|
||||
public ?array $userLocation = null;
|
||||
public bool $locationEnabled = false;
|
||||
public bool $locationLoading = false;
|
||||
|
||||
// UI State
|
||||
public bool $showFilters = false;
|
||||
public bool $isLoading = false;
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated hook for reactive properties
|
||||
*/
|
||||
public function updated($property): void
|
||||
{
|
||||
if (in_array($property, [
|
||||
'search', 'operatorId', 'region', 'country', 'parkType',
|
||||
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
|
||||
'minRides', 'maxDistance', 'sortBy', 'sortDirection'
|
||||
])) {
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable location services
|
||||
*/
|
||||
public function enableLocation(): void
|
||||
{
|
||||
$this->locationLoading = true;
|
||||
$this->dispatch('request-location');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle location received from JavaScript
|
||||
*/
|
||||
public function locationReceived(array $location): void
|
||||
{
|
||||
$this->userLocation = $location;
|
||||
$this->locationEnabled = true;
|
||||
$this->locationLoading = false;
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle location error
|
||||
*/
|
||||
public function locationError(string $error): void
|
||||
{
|
||||
$this->locationLoading = false;
|
||||
$this->dispatch('location-error', message: $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset([
|
||||
'search', 'operatorId', 'region', 'country', 'parkType',
|
||||
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
|
||||
'minRides', 'maxDistance'
|
||||
]);
|
||||
$this->userLocation = null;
|
||||
$this->locationEnabled = false;
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle filters visibility
|
||||
*/
|
||||
public function toggleFilters(): void
|
||||
{
|
||||
$this->showFilters = !$this->showFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort parks by given field
|
||||
*/
|
||||
public function sortBy(string $field): void
|
||||
{
|
||||
if ($this->sortBy === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $field;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parks with location-aware search and filtering
|
||||
*/
|
||||
public function getParksProperty()
|
||||
{
|
||||
return $this->remember('parks', function () {
|
||||
return $this->buildParksQuery()
|
||||
->paginate(12, ['*'], 'page', $this->getPage());
|
||||
}, 1200); // 20-minute location-aware caching
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the parks query with Django parity search and filtering
|
||||
*/
|
||||
protected function buildParksQuery(): Builder
|
||||
{
|
||||
$query = Park::query();
|
||||
|
||||
// Location-aware search functionality (Django parity)
|
||||
if (!empty($this->search)) {
|
||||
$query = $this->locationAwareSearch($query, $this->search, $this->userLocation);
|
||||
}
|
||||
|
||||
// Apply advanced filters with geographic context
|
||||
$query = $this->applyFilters($query, [
|
||||
'operator_id' => $this->operatorId,
|
||||
'region' => $this->region,
|
||||
'country' => $this->country,
|
||||
'park_type' => $this->parkType,
|
||||
'opening_year_from' => $this->openingYearFrom,
|
||||
'opening_year_to' => $this->openingYearTo,
|
||||
'min_area' => $this->minArea,
|
||||
'max_area' => $this->maxArea,
|
||||
'min_rides' => $this->minRides,
|
||||
'max_distance' => $this->maxDistance,
|
||||
], $this->userLocation);
|
||||
|
||||
// Apply sorting with location-aware options
|
||||
$query = $this->applySorting($query);
|
||||
|
||||
// Eager load relationships for performance
|
||||
$query->with(['location', 'operator', 'photos', 'statistics'])
|
||||
->withCount(['rides', 'reviews']);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Location-aware search functionality matching Django implementation
|
||||
*/
|
||||
protected function locationAwareSearch(Builder $query, string $searchQuery, ?array $userLocation = null): Builder
|
||||
{
|
||||
$terms = explode(' ', trim($searchQuery));
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$term = trim($term);
|
||||
if (empty($term)) continue;
|
||||
|
||||
$query->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhere('park_type', 'ilike', "%{$term}%")
|
||||
->orWhereHas('location', function($locQuery) use ($term) {
|
||||
$locQuery->where('city', 'ilike', "%{$term}%")
|
||||
->orWhere('state', 'ilike', "%{$term}%")
|
||||
->orWhere('country', 'ilike', "%{$term}%");
|
||||
})
|
||||
->orWhereHas('operator', fn($opQuery) =>
|
||||
$opQuery->where('name', 'ilike', "%{$term}%"));
|
||||
});
|
||||
}
|
||||
|
||||
// Add distance-based ordering for location-aware results
|
||||
if ($userLocation) {
|
||||
$query->selectRaw('parks.*,
|
||||
(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) *
|
||||
cos(radians(locations.longitude) - radians(?)) +
|
||||
sin(radians(?)) * sin(radians(locations.latitude)))) AS distance',
|
||||
[$userLocation['lat'], $userLocation['lng'], $userLocation['lat']])
|
||||
->join('locations', 'parks.location_id', '=', 'locations.id')
|
||||
->orderBy('distance');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply advanced filters with geographic context
|
||||
*/
|
||||
protected function applyFilters(Builder $query, array $filters, ?array $userLocation = null): Builder
|
||||
{
|
||||
return $query
|
||||
->when($filters['operator_id'] ?? null, fn($q, $operatorId) =>
|
||||
$q->where('operator_id', $operatorId))
|
||||
->when($filters['region'] ?? null, fn($q, $region) =>
|
||||
$q->whereHas('location', fn($locQ) => $locQ->where('state', $region)))
|
||||
->when($filters['country'] ?? null, fn($q, $country) =>
|
||||
$q->whereHas('location', fn($locQ) => $locQ->where('country', $country)))
|
||||
->when($filters['park_type'] ?? null, fn($q, $type) =>
|
||||
$q->where('park_type', $type))
|
||||
->when($filters['opening_year_from'] ?? null, fn($q, $year) =>
|
||||
$q->where('opening_date', '>=', "{$year}-01-01"))
|
||||
->when($filters['opening_year_to'] ?? null, fn($q, $year) =>
|
||||
$q->where('opening_date', '<=', "{$year}-12-31"))
|
||||
->when($filters['min_area'] ?? null, fn($q, $area) =>
|
||||
$q->where('area_acres', '>=', $area))
|
||||
->when($filters['max_area'] ?? null, fn($q, $area) =>
|
||||
$q->where('area_acres', '<=', $area))
|
||||
->when($filters['min_rides'] ?? null, fn($q, $count) =>
|
||||
$q->whereHas('rides', fn($rideQ) => $rideQ->havingRaw('COUNT(*) >= ?', [$count])))
|
||||
->when($filters['max_distance'] ?? null && $userLocation, function($q) use ($filters, $userLocation) {
|
||||
$q->whereRaw('(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) *
|
||||
cos(radians(locations.longitude) - radians(?)) +
|
||||
sin(radians(?)) * sin(radians(locations.latitude)))) <= ?',
|
||||
[$userLocation['lat'], $userLocation['lng'], $userLocation['lat'], $filters['max_distance']]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply sorting with location-aware options
|
||||
*/
|
||||
protected function applySorting(Builder $query): Builder
|
||||
{
|
||||
switch ($this->sortBy) {
|
||||
case 'distance':
|
||||
if ($this->userLocation) {
|
||||
// Distance sorting already applied in locationAwareSearch
|
||||
return $query;
|
||||
}
|
||||
// Fallback to name if no location
|
||||
return $query->orderBy('name', $this->sortDirection);
|
||||
|
||||
case 'rides_count':
|
||||
return $query->orderBy('rides_count', $this->sortDirection);
|
||||
|
||||
case 'reviews_count':
|
||||
return $query->orderBy('reviews_count', $this->sortDirection);
|
||||
|
||||
case 'opening_date':
|
||||
return $query->orderBy('opening_date', $this->sortDirection);
|
||||
|
||||
case 'area_acres':
|
||||
return $query->orderBy('area_acres', $this->sortDirection);
|
||||
|
||||
case 'name':
|
||||
default:
|
||||
return $query->orderBy('name', $this->sortDirection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available operators for filter dropdown
|
||||
*/
|
||||
public function getOperatorsProperty()
|
||||
{
|
||||
return $this->remember('operators', function () {
|
||||
return Operator::select('id', 'name')
|
||||
->whereHas('parks')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available regions for filter dropdown
|
||||
*/
|
||||
public function getRegionsProperty()
|
||||
{
|
||||
return $this->remember('regions', function () {
|
||||
return DB::table('locations')
|
||||
->join('parks', 'locations.id', '=', 'parks.location_id')
|
||||
->select('locations.state')
|
||||
->distinct()
|
||||
->whereNotNull('locations.state')
|
||||
->orderBy('locations.state')
|
||||
->pluck('state');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available countries for filter dropdown
|
||||
*/
|
||||
public function getCountriesProperty()
|
||||
{
|
||||
return $this->remember('countries', function () {
|
||||
return DB::table('locations')
|
||||
->join('parks', 'locations.id', '=', 'parks.location_id')
|
||||
->select('locations.country')
|
||||
->distinct()
|
||||
->whereNotNull('locations.country')
|
||||
->orderBy('locations.country')
|
||||
->pluck('country');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available park types for filter dropdown
|
||||
*/
|
||||
public function getParkTypesProperty()
|
||||
{
|
||||
return $this->remember('park_types', function () {
|
||||
return Park::select('park_type')
|
||||
->distinct()
|
||||
->whereNotNull('park_type')
|
||||
->orderBy('park_type')
|
||||
->pluck('park_type');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filter summary for display
|
||||
*/
|
||||
public function getActiveFiltersProperty(): array
|
||||
{
|
||||
$filters = [];
|
||||
|
||||
if (!empty($this->search)) {
|
||||
$filters[] = "Search: {$this->search}";
|
||||
}
|
||||
|
||||
if (!empty($this->operatorId)) {
|
||||
$operator = $this->operators->find($this->operatorId);
|
||||
if ($operator) {
|
||||
$filters[] = "Operator: {$operator->name}";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->region)) {
|
||||
$filters[] = "Region: {$this->region}";
|
||||
}
|
||||
|
||||
if (!empty($this->country)) {
|
||||
$filters[] = "Country: {$this->country}";
|
||||
}
|
||||
|
||||
if (!empty($this->parkType)) {
|
||||
$filters[] = "Type: {$this->parkType}";
|
||||
}
|
||||
|
||||
if (!empty($this->openingYearFrom) || !empty($this->openingYearTo)) {
|
||||
$yearRange = $this->openingYearFrom . ' - ' . $this->openingYearTo;
|
||||
$filters[] = "Years: {$yearRange}";
|
||||
}
|
||||
|
||||
if (!empty($this->minArea) || !empty($this->maxArea)) {
|
||||
$areaRange = $this->minArea . ' - ' . $this->maxArea . ' acres';
|
||||
$filters[] = "Area: {$areaRange}";
|
||||
}
|
||||
|
||||
if (!empty($this->minRides)) {
|
||||
$filters[] = "Min Rides: {$this->minRides}";
|
||||
}
|
||||
|
||||
if (!empty($this->maxDistance) && $this->locationEnabled) {
|
||||
$filters[] = "Within: {$this->maxDistance} km";
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$this->isLoading = false;
|
||||
|
||||
return view('livewire.parks-listing', [
|
||||
'parks' => $this->parks,
|
||||
'operators' => $this->operators,
|
||||
'regions' => $this->regions,
|
||||
'countries' => $this->countries,
|
||||
'parkTypes' => $this->parkTypes,
|
||||
'activeFilters' => $this->activeFilters,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
$locationKey = $this->userLocation ?
|
||||
md5(json_encode($this->userLocation)) : 'no-location';
|
||||
|
||||
$filterKey = md5(serialize([
|
||||
$this->search, $this->operatorId, $this->region, $this->country,
|
||||
$this->parkType, $this->openingYearFrom, $this->openingYearTo,
|
||||
$this->minArea, $this->maxArea, $this->minRides, $this->maxDistance,
|
||||
$this->sortBy, $this->sortDirection
|
||||
]));
|
||||
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' .
|
||||
$locationKey . '.' . $filterKey . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
475
app/Livewire/ParksListingUniversal.php
Normal file
475
app/Livewire/ParksListingUniversal.php
Normal file
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Operator;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ParksListingUniversal extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Universal Listing System Integration
|
||||
public string $entityType = 'parks';
|
||||
|
||||
// Search and Filter Properties with URL binding
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'operator')]
|
||||
public string $operatorId = '';
|
||||
|
||||
#[Url(as: 'region')]
|
||||
public string $region = '';
|
||||
|
||||
#[Url(as: 'country')]
|
||||
public string $country = '';
|
||||
|
||||
#[Url(as: 'type')]
|
||||
public string $parkType = '';
|
||||
|
||||
#[Url(as: 'year_from')]
|
||||
public string $openingYearFrom = '';
|
||||
|
||||
#[Url(as: 'year_to')]
|
||||
public string $openingYearTo = '';
|
||||
|
||||
#[Url(as: 'min_area')]
|
||||
public string $minArea = '';
|
||||
|
||||
#[Url(as: 'max_area')]
|
||||
public string $maxArea = '';
|
||||
|
||||
#[Url(as: 'min_rides')]
|
||||
public string $minRides = '';
|
||||
|
||||
#[Url(as: 'max_distance')]
|
||||
public string $maxDistance = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'dir')]
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
// Location Properties (GPS Integration)
|
||||
public ?array $userLocation = null;
|
||||
public bool $locationEnabled = false;
|
||||
public bool $locationLoading = false;
|
||||
|
||||
// UI State
|
||||
public bool $showFilters = false;
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updated hook for reactive properties
|
||||
*/
|
||||
public function updated($property): void
|
||||
{
|
||||
if (in_array($property, [
|
||||
'search', 'operatorId', 'region', 'country', 'parkType',
|
||||
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
|
||||
'minRides', 'maxDistance', 'sortBy', 'sortDirection'
|
||||
])) {
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable location services (GPS Integration)
|
||||
*/
|
||||
public function enableLocation(): void
|
||||
{
|
||||
$this->locationLoading = true;
|
||||
$this->dispatch('request-location');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle location received from JavaScript
|
||||
*/
|
||||
public function locationReceived(array $location): void
|
||||
{
|
||||
$this->userLocation = $location;
|
||||
$this->locationEnabled = true;
|
||||
$this->locationLoading = false;
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle location error
|
||||
*/
|
||||
public function locationError(string $error): void
|
||||
{
|
||||
$this->locationLoading = false;
|
||||
$this->dispatch('location-error', message: $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset([
|
||||
'search', 'operatorId', 'region', 'country', 'parkType',
|
||||
'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea',
|
||||
'minRides', 'maxDistance'
|
||||
]);
|
||||
$this->userLocation = null;
|
||||
$this->locationEnabled = false;
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle filters visibility
|
||||
*/
|
||||
public function toggleFilters(): void
|
||||
{
|
||||
$this->showFilters = !$this->showFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort parks by given field
|
||||
*/
|
||||
public function sortBy(string $field): void
|
||||
{
|
||||
if ($this->sortBy === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortBy = $field;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('parks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parks with location-aware search and filtering (Django parity)
|
||||
*/
|
||||
public function getParksProperty()
|
||||
{
|
||||
return $this->remember('parks', function () {
|
||||
return $this->buildParksQuery()
|
||||
->paginate(12, ['*'], 'page', $this->getPage());
|
||||
}, 1200); // 20-minute location-aware caching
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the parks query with Django parity search and filtering
|
||||
*/
|
||||
protected function buildParksQuery(): Builder
|
||||
{
|
||||
$query = Park::query();
|
||||
|
||||
// Location-aware search functionality (Django parity)
|
||||
if (!empty($this->search)) {
|
||||
$query = $this->locationAwareSearch($query, $this->search, $this->userLocation);
|
||||
}
|
||||
|
||||
// Apply advanced filters with geographic context
|
||||
$query = $this->applyFilters($query, [
|
||||
'operator_id' => $this->operatorId,
|
||||
'region' => $this->region,
|
||||
'country' => $this->country,
|
||||
'park_type' => $this->parkType,
|
||||
'opening_year_from' => $this->openingYearFrom,
|
||||
'opening_year_to' => $this->openingYearTo,
|
||||
'min_area' => $this->minArea,
|
||||
'max_area' => $this->maxArea,
|
||||
'min_rides' => $this->minRides,
|
||||
'max_distance' => $this->maxDistance,
|
||||
], $this->userLocation);
|
||||
|
||||
// Apply sorting with location-aware options
|
||||
$query = $this->applySorting($query);
|
||||
|
||||
// Eager load relationships for performance
|
||||
$query->with(['location', 'operator', 'photos', 'statistics'])
|
||||
->withCount(['rides', 'reviews']);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Location-aware search functionality matching Django implementation
|
||||
*/
|
||||
protected function locationAwareSearch(Builder $query, string $searchQuery, ?array $userLocation = null): Builder
|
||||
{
|
||||
$terms = explode(' ', trim($searchQuery));
|
||||
|
||||
foreach ($terms as $term) {
|
||||
$term = trim($term);
|
||||
if (empty($term)) continue;
|
||||
|
||||
$query->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhere('park_type', 'ilike', "%{$term}%")
|
||||
->orWhereHas('location', function($locQuery) use ($term) {
|
||||
$locQuery->where('city', 'ilike', "%{$term}%")
|
||||
->orWhere('state', 'ilike', "%{$term}%")
|
||||
->orWhere('country', 'ilike', "%{$term}%");
|
||||
})
|
||||
->orWhereHas('operator', fn($opQuery) =>
|
||||
$opQuery->where('name', 'ilike', "%{$term}%"));
|
||||
});
|
||||
}
|
||||
|
||||
// Add distance-based ordering for location-aware results
|
||||
if ($userLocation) {
|
||||
$query->selectRaw('parks.*,
|
||||
(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) *
|
||||
cos(radians(locations.longitude) - radians(?)) +
|
||||
sin(radians(?)) * sin(radians(locations.latitude)))) AS distance',
|
||||
[$userLocation['lat'], $userLocation['lng'], $userLocation['lat']])
|
||||
->join('locations', 'parks.location_id', '=', 'locations.id')
|
||||
->orderBy('distance');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply advanced filters with geographic context
|
||||
*/
|
||||
protected function applyFilters(Builder $query, array $filters, ?array $userLocation = null): Builder
|
||||
{
|
||||
return $query
|
||||
->when($filters['operator_id'] ?? null, fn($q, $operatorId) =>
|
||||
$q->where('operator_id', $operatorId))
|
||||
->when($filters['region'] ?? null, fn($q, $region) =>
|
||||
$q->whereHas('location', fn($locQ) => $locQ->where('state', $region)))
|
||||
->when($filters['country'] ?? null, fn($q, $country) =>
|
||||
$q->whereHas('location', fn($locQ) => $locQ->where('country', $country)))
|
||||
->when($filters['park_type'] ?? null, fn($q, $type) =>
|
||||
$q->where('park_type', $type))
|
||||
->when($filters['opening_year_from'] ?? null, fn($q, $year) =>
|
||||
$q->where('opening_date', '>=', "{$year}-01-01"))
|
||||
->when($filters['opening_year_to'] ?? null, fn($q, $year) =>
|
||||
$q->where('opening_date', '<=', "{$year}-12-31"))
|
||||
->when($filters['min_area'] ?? null, fn($q, $area) =>
|
||||
$q->where('area_acres', '>=', $area))
|
||||
->when($filters['max_area'] ?? null, fn($q, $area) =>
|
||||
$q->where('area_acres', '<=', $area))
|
||||
->when($filters['min_rides'] ?? null, fn($q, $count) =>
|
||||
$q->whereHas('rides', fn($rideQ) => $rideQ->havingRaw('COUNT(*) >= ?', [$count])))
|
||||
->when($filters['max_distance'] ?? null && $userLocation, function($q) use ($filters, $userLocation) {
|
||||
$q->whereRaw('(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) *
|
||||
cos(radians(locations.longitude) - radians(?)) +
|
||||
sin(radians(?)) * sin(radians(locations.latitude)))) <= ?',
|
||||
[$userLocation['lat'], $userLocation['lng'], $userLocation['lat'], $filters['max_distance']]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply sorting with location-aware options
|
||||
*/
|
||||
protected function applySorting(Builder $query): Builder
|
||||
{
|
||||
switch ($this->sortBy) {
|
||||
case 'distance':
|
||||
if ($this->userLocation) {
|
||||
// Distance sorting already applied in locationAwareSearch
|
||||
return $query;
|
||||
}
|
||||
// Fallback to name if no location
|
||||
return $query->orderBy('name', $this->sortDirection);
|
||||
|
||||
case 'rides_count':
|
||||
return $query->orderBy('rides_count', $this->sortDirection);
|
||||
|
||||
case 'reviews_count':
|
||||
return $query->orderBy('reviews_count', $this->sortDirection);
|
||||
|
||||
case 'opening_date':
|
||||
return $query->orderBy('opening_date', $this->sortDirection);
|
||||
|
||||
case 'area_acres':
|
||||
return $query->orderBy('area_acres', $this->sortDirection);
|
||||
|
||||
case 'name':
|
||||
default:
|
||||
return $query->orderBy('name', $this->sortDirection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available operators for filter dropdown
|
||||
*/
|
||||
public function getOperatorsProperty()
|
||||
{
|
||||
return $this->remember('operators', function () {
|
||||
return Operator::select('id', 'name')
|
||||
->whereHas('parks')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available regions for filter dropdown
|
||||
*/
|
||||
public function getRegionsProperty()
|
||||
{
|
||||
return $this->remember('regions', function () {
|
||||
return DB::table('locations')
|
||||
->join('parks', 'locations.id', '=', 'parks.location_id')
|
||||
->select('locations.state')
|
||||
->distinct()
|
||||
->whereNotNull('locations.state')
|
||||
->orderBy('locations.state')
|
||||
->pluck('state');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available countries for filter dropdown
|
||||
*/
|
||||
public function getCountriesProperty()
|
||||
{
|
||||
return $this->remember('countries', function () {
|
||||
return DB::table('locations')
|
||||
->join('parks', 'locations.id', '=', 'parks.location_id')
|
||||
->select('locations.country')
|
||||
->distinct()
|
||||
->whereNotNull('locations.country')
|
||||
->orderBy('locations.country')
|
||||
->pluck('country');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available park types for filter dropdown
|
||||
*/
|
||||
public function getParkTypesProperty()
|
||||
{
|
||||
return $this->remember('park_types', function () {
|
||||
return Park::select('park_type')
|
||||
->distinct()
|
||||
->whereNotNull('park_type')
|
||||
->orderBy('park_type')
|
||||
->pluck('park_type');
|
||||
}, 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filter summary for display
|
||||
*/
|
||||
public function getActiveFiltersProperty(): array
|
||||
{
|
||||
$filters = [];
|
||||
|
||||
if (!empty($this->search)) {
|
||||
$filters[] = "Search: {$this->search}";
|
||||
}
|
||||
|
||||
if (!empty($this->operatorId)) {
|
||||
$operator = $this->operators->find($this->operatorId);
|
||||
if ($operator) {
|
||||
$filters[] = "Operator: {$operator->name}";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($this->region)) {
|
||||
$filters[] = "Region: {$this->region}";
|
||||
}
|
||||
|
||||
if (!empty($this->country)) {
|
||||
$filters[] = "Country: {$this->country}";
|
||||
}
|
||||
|
||||
if (!empty($this->parkType)) {
|
||||
$filters[] = "Type: {$this->parkType}";
|
||||
}
|
||||
|
||||
if (!empty($this->openingYearFrom) || !empty($this->openingYearTo)) {
|
||||
$yearRange = $this->openingYearFrom . ' - ' . $this->openingYearTo;
|
||||
$filters[] = "Years: {$yearRange}";
|
||||
}
|
||||
|
||||
if (!empty($this->minArea) || !empty($this->maxArea)) {
|
||||
$areaRange = $this->minArea . ' - ' . $this->maxArea . ' acres';
|
||||
$filters[] = "Area: {$areaRange}";
|
||||
}
|
||||
|
||||
if (!empty($this->minRides)) {
|
||||
$filters[] = "Min Rides: {$this->minRides}";
|
||||
}
|
||||
|
||||
if (!empty($this->maxDistance) && $this->locationEnabled) {
|
||||
$filters[] = "Within: {$this->maxDistance} km";
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component using Universal Listing System
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.parks-listing-universal', [
|
||||
'parks' => $this->parks,
|
||||
'operators' => $this->operators,
|
||||
'regions' => $this->regions,
|
||||
'countries' => $this->countries,
|
||||
'parkTypes' => $this->parkTypes,
|
||||
'activeFilters' => $this->activeFilters,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
$locationKey = $this->userLocation ?
|
||||
md5(json_encode($this->userLocation)) : 'no-location';
|
||||
|
||||
$filterKey = md5(serialize([
|
||||
$this->search, $this->operatorId, $this->region, $this->country,
|
||||
$this->parkType, $this->openingYearFrom, $this->openingYearTo,
|
||||
$this->minArea, $this->maxArea, $this->minRides, $this->maxDistance,
|
||||
$this->sortBy, $this->sortDirection
|
||||
]));
|
||||
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' .
|
||||
$locationKey . '.' . $filterKey . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/ParksLocationSearch.php
Normal file
54
app/Livewire/ParksLocationSearch.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ParksLocationSearch extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.parks-location-search');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
app/Livewire/ParksMapView.php
Normal file
54
app/Livewire/ParksMapView.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ParksMapView extends Component
|
||||
{
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.parks-map-view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
108
app/Livewire/PhotoGalleryComponent.php
Normal file
108
app/Livewire/PhotoGalleryComponent.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use App\Models\Photo;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PhotoGalleryComponent extends Component
|
||||
{
|
||||
public Park $park;
|
||||
public $photos = [];
|
||||
public $featuredPhoto = null;
|
||||
public $selectedPhoto = null;
|
||||
public $isLoading = true;
|
||||
public $error = null;
|
||||
public $viewMode = 'grid'; // grid or carousel
|
||||
|
||||
protected $listeners = ['photoUploaded' => 'loadPhotos'];
|
||||
|
||||
public function mount(Park $park)
|
||||
{
|
||||
$this->park = $park;
|
||||
$this->loadPhotos();
|
||||
}
|
||||
|
||||
public function loadPhotos()
|
||||
{
|
||||
$this->isLoading = true;
|
||||
$this->error = null;
|
||||
|
||||
try {
|
||||
$this->photos = $this->park->photos()->ordered()->get();
|
||||
$this->featuredPhoto = $this->park->featuredPhoto();
|
||||
$this->isLoading = false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error loading photos: ' . $e->getMessage());
|
||||
$this->error = 'Failed to load photos: ' . $e->getMessage();
|
||||
$this->isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function selectPhoto($photoId)
|
||||
{
|
||||
$this->selectedPhoto = collect($this->photos)->firstWhere('id', $photoId);
|
||||
}
|
||||
|
||||
public function closePhotoDetail()
|
||||
{
|
||||
$this->selectedPhoto = null;
|
||||
}
|
||||
|
||||
public function setFeatured($photoId)
|
||||
{
|
||||
try {
|
||||
$photo = Photo::findOrFail($photoId);
|
||||
$this->park->setFeaturedPhoto($photo);
|
||||
$this->loadPhotos();
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'success',
|
||||
'message' => 'Featured photo updated successfully'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error setting featured photo: ' . $e->getMessage());
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'error',
|
||||
'message' => 'Failed to set featured photo'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function deletePhoto($photoId)
|
||||
{
|
||||
try {
|
||||
$photo = Photo::findOrFail($photoId);
|
||||
|
||||
// Make API request to the PhotoController
|
||||
app(\App\Http\Controllers\PhotoController::class)->destroy($photo);
|
||||
|
||||
$this->loadPhotos();
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'success',
|
||||
'message' => 'Photo deleted successfully'
|
||||
]);
|
||||
|
||||
if ($this->selectedPhoto && $this->selectedPhoto->id === $photoId) {
|
||||
$this->selectedPhoto = null;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting photo: ' . $e->getMessage());
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'error',
|
||||
'message' => 'Failed to delete photo: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleViewMode()
|
||||
{
|
||||
$this->viewMode = $this->viewMode === 'grid' ? 'carousel' : 'grid';
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.photo-gallery-component');
|
||||
}
|
||||
}
|
||||
100
app/Livewire/PhotoManagerComponent.php
Normal file
100
app/Livewire/PhotoManagerComponent.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PhotoManagerComponent extends Component
|
||||
{
|
||||
public Park $park;
|
||||
public $photos = [];
|
||||
public $isLoading = true;
|
||||
public $error = null;
|
||||
public $reordering = false;
|
||||
public $photoOrder = [];
|
||||
|
||||
protected $listeners = ['photoUploaded' => 'loadPhotos'];
|
||||
|
||||
public function mount(Park $park)
|
||||
{
|
||||
$this->park = $park;
|
||||
$this->loadPhotos();
|
||||
}
|
||||
|
||||
public function loadPhotos()
|
||||
{
|
||||
$this->isLoading = true;
|
||||
$this->error = null;
|
||||
|
||||
try {
|
||||
$this->photos = $this->park->photos()->ordered()->get()->toArray();
|
||||
$this->photoOrder = collect($this->photos)->pluck('id')->toArray();
|
||||
$this->isLoading = false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error loading photos: ' . $e->getMessage());
|
||||
$this->error = 'Failed to load photos: ' . $e->getMessage();
|
||||
$this->isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function startReordering()
|
||||
{
|
||||
$this->reordering = true;
|
||||
}
|
||||
|
||||
public function cancelReordering()
|
||||
{
|
||||
$this->reordering = false;
|
||||
$this->loadPhotos(); // Reset to original order
|
||||
}
|
||||
|
||||
public function saveOrder()
|
||||
{
|
||||
try {
|
||||
// Make API request to the PhotoController
|
||||
app(\App\Http\Controllers\PhotoController::class)->reorder(
|
||||
request: new \Illuminate\Http\Request(['photo_ids' => $this->photoOrder]),
|
||||
park: $this->park
|
||||
);
|
||||
|
||||
$this->reordering = false;
|
||||
$this->loadPhotos();
|
||||
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'success',
|
||||
'message' => 'Photo order updated successfully'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error reordering photos: ' . $e->getMessage());
|
||||
$this->dispatch('notify', [
|
||||
'type' => 'error',
|
||||
'message' => 'Failed to update photo order: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function moveUp($index)
|
||||
{
|
||||
if ($index > 0) {
|
||||
$temp = $this->photoOrder[$index - 1];
|
||||
$this->photoOrder[$index - 1] = $this->photoOrder[$index];
|
||||
$this->photoOrder[$index] = $temp;
|
||||
}
|
||||
}
|
||||
|
||||
public function moveDown($index)
|
||||
{
|
||||
if ($index < count($this->photoOrder) - 1) {
|
||||
$temp = $this->photoOrder[$index + 1];
|
||||
$this->photoOrder[$index + 1] = $this->photoOrder[$index];
|
||||
$this->photoOrder[$index] = $temp;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.photo-manager-component');
|
||||
}
|
||||
}
|
||||
92
app/Livewire/PhotoUploadComponent.php
Normal file
92
app/Livewire/PhotoUploadComponent.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PhotoUploadComponent extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public Park $park;
|
||||
public $photo;
|
||||
public $title;
|
||||
public $description;
|
||||
public $alt_text;
|
||||
public $credit;
|
||||
public $source_url;
|
||||
public $is_featured = false;
|
||||
public $uploading = false;
|
||||
public $uploadError = null;
|
||||
public $uploadSuccess = false;
|
||||
|
||||
protected $rules = [
|
||||
'photo' => 'required|image|max:10240', // 10MB max
|
||||
'title' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'alt_text' => 'nullable|string|max:255',
|
||||
'credit' => 'nullable|string|max:255',
|
||||
'source_url' => 'nullable|url|max:255',
|
||||
'is_featured' => 'boolean',
|
||||
];
|
||||
|
||||
public function mount(Park $park)
|
||||
{
|
||||
$this->park = $park;
|
||||
}
|
||||
|
||||
public function updatedPhoto()
|
||||
{
|
||||
$this->validate([
|
||||
'photo' => 'image|max:10240', // 10MB max
|
||||
]);
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->uploading = true;
|
||||
$this->uploadError = null;
|
||||
$this->uploadSuccess = false;
|
||||
|
||||
try {
|
||||
$this->validate();
|
||||
|
||||
// Create form data for the API request
|
||||
$formData = [
|
||||
'photo' => $this->photo,
|
||||
'title' => $this->title,
|
||||
'description' => $this->description,
|
||||
'alt_text' => $this->alt_text,
|
||||
'credit' => $this->credit,
|
||||
'source_url' => $this->source_url,
|
||||
'is_featured' => $this->is_featured,
|
||||
];
|
||||
|
||||
// Make API request to the PhotoController
|
||||
$response = app(\App\Http\Controllers\PhotoController::class)->store(
|
||||
request: new \Illuminate\Http\Request($formData),
|
||||
park: $this->park
|
||||
);
|
||||
|
||||
// Reset form
|
||||
$this->reset(['photo', 'title', 'description', 'alt_text', 'credit', 'source_url', 'is_featured']);
|
||||
$this->uploadSuccess = true;
|
||||
|
||||
// Emit event to refresh the photo gallery
|
||||
$this->dispatch('photoUploaded');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Photo upload error: ' . $e->getMessage());
|
||||
$this->uploadError = 'Failed to upload photo: ' . $e->getMessage();
|
||||
} finally {
|
||||
$this->uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.photo-upload-component');
|
||||
}
|
||||
}
|
||||
57
app/Livewire/RegionalParksListing.php
Normal file
57
app/Livewire/RegionalParksListing.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class RegionalParksListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.regional-parks-listing');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
227
app/Livewire/ReviewModerationComponent.php
Normal file
227
app/Livewire/ReviewModerationComponent.php
Normal file
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Review;
|
||||
use App\Enums\ReviewStatus;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class ReviewModerationComponent extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
/**
|
||||
* Current filter status
|
||||
*/
|
||||
public ?string $statusFilter = null;
|
||||
|
||||
/**
|
||||
* Search query
|
||||
*/
|
||||
public string $search = '';
|
||||
|
||||
/**
|
||||
* Selected reviews for batch actions
|
||||
*/
|
||||
public array $selected = [];
|
||||
|
||||
/**
|
||||
* Whether to show the edit modal
|
||||
*/
|
||||
public bool $showEditModal = false;
|
||||
|
||||
/**
|
||||
* Review being edited
|
||||
*/
|
||||
public ?Review $editingReview = null;
|
||||
|
||||
/**
|
||||
* Form fields for editing
|
||||
*/
|
||||
public array $form = [
|
||||
'rating' => null,
|
||||
'title' => null,
|
||||
'content' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* Success/error message
|
||||
*/
|
||||
public ?string $message = null;
|
||||
|
||||
/**
|
||||
* Validation rules
|
||||
*/
|
||||
protected function rules()
|
||||
{
|
||||
return [
|
||||
'form.rating' => 'required|integer|min:1|max:5',
|
||||
'form.title' => 'nullable|string|max:100',
|
||||
'form.content' => 'required|string|min:10|max:2000',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount the component
|
||||
*/
|
||||
public function mount()
|
||||
{
|
||||
$this->statusFilter = ReviewStatus::PENDING->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by status
|
||||
*/
|
||||
public function filterByStatus(?string $status)
|
||||
{
|
||||
$this->statusFilter = $status;
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show edit modal for a review
|
||||
*/
|
||||
public function editReview(Review $review)
|
||||
{
|
||||
$this->editingReview = $review;
|
||||
$this->form = [
|
||||
'rating' => $review->rating,
|
||||
'title' => $review->title,
|
||||
'content' => $review->content,
|
||||
];
|
||||
$this->showEditModal = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save edited review
|
||||
*/
|
||||
public function saveEdit()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
$this->editingReview->update([
|
||||
'rating' => $this->form['rating'],
|
||||
'title' => $this->form['title'],
|
||||
'content' => $this->form['content'],
|
||||
'moderated_at' => now(),
|
||||
'moderated_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
$this->message = 'Review updated successfully.';
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['editingReview', 'form']);
|
||||
} catch (\Exception $e) {
|
||||
$this->message = 'An error occurred while updating the review.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a review
|
||||
*/
|
||||
public function approve(Review $review)
|
||||
{
|
||||
try {
|
||||
$review->update([
|
||||
'status' => ReviewStatus::APPROVED,
|
||||
'moderated_at' => now(),
|
||||
'moderated_by' => Auth::id(),
|
||||
]);
|
||||
$this->message = 'Review approved successfully.';
|
||||
} catch (\Exception $e) {
|
||||
$this->message = 'An error occurred while approving the review.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a review
|
||||
*/
|
||||
public function reject(Review $review)
|
||||
{
|
||||
try {
|
||||
$review->update([
|
||||
'status' => ReviewStatus::REJECTED,
|
||||
'moderated_at' => now(),
|
||||
'moderated_by' => Auth::id(),
|
||||
]);
|
||||
$this->message = 'Review rejected successfully.';
|
||||
} catch (\Exception $e) {
|
||||
$this->message = 'An error occurred while rejecting the review.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch approve selected reviews
|
||||
*/
|
||||
public function batchApprove()
|
||||
{
|
||||
try {
|
||||
Review::whereIn('id', $this->selected)->update([
|
||||
'status' => ReviewStatus::APPROVED,
|
||||
'moderated_at' => now(),
|
||||
'moderated_by' => Auth::id(),
|
||||
]);
|
||||
$this->message = count($this->selected) . ' reviews approved successfully.';
|
||||
$this->selected = [];
|
||||
} catch (\Exception $e) {
|
||||
$this->message = 'An error occurred while approving the reviews.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch reject selected reviews
|
||||
*/
|
||||
public function batchReject()
|
||||
{
|
||||
try {
|
||||
Review::whereIn('id', $this->selected)->update([
|
||||
'status' => ReviewStatus::REJECTED,
|
||||
'moderated_at' => now(),
|
||||
'moderated_by' => Auth::id(),
|
||||
]);
|
||||
$this->message = count($this->selected) . ' reviews rejected successfully.';
|
||||
$this->selected = [];
|
||||
} catch (\Exception $e) {
|
||||
$this->message = 'An error occurred while rejecting the reviews.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reviews query
|
||||
*/
|
||||
protected function getReviewsQuery()
|
||||
{
|
||||
$query = Review::with(['user', 'ride'])
|
||||
->when($this->statusFilter, function ($query, $status) {
|
||||
$query->where('status', $status);
|
||||
})
|
||||
->when($this->search, function ($query, $search) {
|
||||
$query->where(function ($query) use ($search) {
|
||||
$query->where('title', 'like', "%{$search}%")
|
||||
->orWhere('content', 'like', "%{$search}%")
|
||||
->orWhereHas('user', function ($query) use ($search) {
|
||||
$query->where('name', 'like', "%{$search}%");
|
||||
})
|
||||
->orWhereHas('ride', function ($query) use ($search) {
|
||||
$query->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
})
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.review-moderation-component', [
|
||||
'reviews' => $this->getReviewsQuery()->paginate(10),
|
||||
'totalPending' => Review::where('status', ReviewStatus::PENDING)->count(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
88
app/Livewire/RideDetailComponent.php
Normal file
88
app/Livewire/RideDetailComponent.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use Livewire\Component;
|
||||
|
||||
class RideDetailComponent extends Component
|
||||
{
|
||||
/** @var Ride */
|
||||
public Ride $ride;
|
||||
|
||||
/** @var bool */
|
||||
public bool $showCoasterStats = false;
|
||||
|
||||
public function mount(Ride $ride): void
|
||||
{
|
||||
$this->ride = $ride->load([
|
||||
'park',
|
||||
'parkArea',
|
||||
'manufacturer',
|
||||
'designer',
|
||||
'rideModel',
|
||||
'coasterStats',
|
||||
]);
|
||||
|
||||
$this->showCoasterStats = $ride->category === 'RC' && $ride->coasterStats !== null;
|
||||
}
|
||||
|
||||
public function toggleCoasterStats(): void
|
||||
{
|
||||
$this->showCoasterStats = !$this->showCoasterStats;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ride-detail');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the opening year of the ride.
|
||||
*/
|
||||
public function getOpeningYearAttribute(): ?string
|
||||
{
|
||||
return $this->ride->opening_date?->format('Y');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a formatted date range for the ride's operation.
|
||||
*/
|
||||
public function getOperatingPeriodAttribute(): string
|
||||
{
|
||||
$start = $this->ride->opening_date?->format('Y');
|
||||
$end = $this->ride->closing_date?->format('Y');
|
||||
|
||||
if (!$start) {
|
||||
return 'Unknown dates';
|
||||
}
|
||||
|
||||
return $end ? "{$start} - {$end}" : "{$start} - Present";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ride's status badge color classes.
|
||||
*/
|
||||
public function getStatusColorClasses(): string
|
||||
{
|
||||
return match($this->ride->status) {
|
||||
'OPERATING' => 'bg-green-100 text-green-800',
|
||||
'CLOSED_TEMP' => 'bg-yellow-100 text-yellow-800',
|
||||
'SBNO' => 'bg-red-100 text-red-800',
|
||||
'CLOSING' => 'bg-orange-100 text-orange-800',
|
||||
'CLOSED_PERM' => 'bg-gray-100 text-gray-800',
|
||||
'UNDER_CONSTRUCTION' => 'bg-blue-100 text-blue-800',
|
||||
'DEMOLISHED' => 'bg-gray-100 text-gray-800',
|
||||
'RELOCATED' => 'bg-purple-100 text-purple-800',
|
||||
default => 'bg-gray-100 text-gray-800',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a measurement value with units.
|
||||
*/
|
||||
public function formatMeasurement(?float $value, string $unit): string
|
||||
{
|
||||
return $value !== null ? number_format($value, 2) . ' ' . $unit : 'N/A';
|
||||
}
|
||||
}
|
||||
140
app/Livewire/RideFormComponent.php
Normal file
140
app/Livewire/RideFormComponent.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Models\Park;
|
||||
use App\Models\ParkArea;
|
||||
use App\Models\RideModel;
|
||||
use App\Models\Designer;
|
||||
use App\Models\Manufacturer;
|
||||
use App\Enums\RideCategory;
|
||||
use App\Enums\RideStatus;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Validation\Rules\Enum;
|
||||
|
||||
class RideFormComponent extends Component
|
||||
{
|
||||
public ?Ride $ride = null;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
public array $state = [];
|
||||
|
||||
/** @var array<string, mixed>|null */
|
||||
public ?array $coasterStats = null;
|
||||
|
||||
/** @var int|null */
|
||||
public ?int $parkId = null;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'state.name' => ['required', 'string', 'max:255'],
|
||||
'state.park_id' => ['required', 'exists:parks,id'],
|
||||
'state.park_area_id' => ['nullable', 'exists:park_areas,id'],
|
||||
'state.manufacturer_id' => ['nullable', 'exists:manufacturers,id'],
|
||||
'state.designer_id' => ['nullable', 'exists:designers,id'],
|
||||
'state.ride_model_id' => ['nullable', 'exists:ride_models,id'],
|
||||
'state.category' => ['required', new Enum(RideCategory::class)],
|
||||
'state.status' => ['required', new Enum(RideStatus::class)],
|
||||
'state.post_closing_status' => ['nullable', new Enum(RideStatus::class)],
|
||||
'state.description' => ['nullable', 'string'],
|
||||
'state.opening_date' => ['nullable', 'date'],
|
||||
'state.closing_date' => ['nullable', 'date'],
|
||||
'state.status_since' => ['nullable', 'date'],
|
||||
'state.min_height_in' => ['nullable', 'integer', 'min:0'],
|
||||
'state.max_height_in' => ['nullable', 'integer', 'min:0'],
|
||||
'state.capacity_per_hour' => ['nullable', 'integer', 'min:0'],
|
||||
'state.ride_duration_seconds' => ['nullable', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
|
||||
public function mount(?Ride $ride = null): void
|
||||
{
|
||||
$this->ride = $ride;
|
||||
|
||||
if ($ride) {
|
||||
$this->state = $ride->only([
|
||||
'name', 'park_id', 'park_area_id', 'manufacturer_id',
|
||||
'designer_id', 'ride_model_id', 'category', 'status',
|
||||
'post_closing_status', 'description', 'opening_date',
|
||||
'closing_date', 'status_since', 'min_height_in',
|
||||
'max_height_in', 'capacity_per_hour', 'ride_duration_seconds',
|
||||
]);
|
||||
|
||||
$this->parkId = $ride->park_id;
|
||||
|
||||
if ($ride->coasterStats) {
|
||||
$this->coasterStats = $ride->coasterStats->toArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updated(string $field): void
|
||||
{
|
||||
$this->validateOnly($field);
|
||||
|
||||
if ($field === 'state.category') {
|
||||
$this->coasterStats = $this->state['category'] === RideCategory::ROLLER_COASTER->value
|
||||
? ($this->coasterStats ?? [])
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
if (!$this->ride) {
|
||||
$this->ride = new Ride();
|
||||
}
|
||||
|
||||
$this->ride->fill($this->state)->save();
|
||||
|
||||
// Handle coaster stats if applicable
|
||||
if ($this->state['category'] === RideCategory::ROLLER_COASTER->value && $this->coasterStats) {
|
||||
$this->ride->coasterStats()->updateOrCreate(
|
||||
['ride_id' => $this->ride->id],
|
||||
$this->coasterStats
|
||||
);
|
||||
} elseif ($this->ride->coasterStats) {
|
||||
$this->ride->coasterStats->delete();
|
||||
}
|
||||
|
||||
session()->flash('message', 'Ride saved successfully.');
|
||||
|
||||
$this->redirect(route('rides.show', $this->ride));
|
||||
}
|
||||
|
||||
public function getParksProperty()
|
||||
{
|
||||
return Park::orderBy('name')->get();
|
||||
}
|
||||
|
||||
public function getParkAreasProperty()
|
||||
{
|
||||
return $this->parkId
|
||||
? ParkArea::where('park_id', $this->parkId)->orderBy('name')->get()
|
||||
: collect();
|
||||
}
|
||||
|
||||
public function getManufacturersProperty()
|
||||
{
|
||||
return Manufacturer::orderBy('name')->get();
|
||||
}
|
||||
|
||||
public function getDesignersProperty()
|
||||
{
|
||||
return Designer::orderBy('name')->get();
|
||||
}
|
||||
|
||||
public function getRideModelsProperty()
|
||||
{
|
||||
return RideModel::orderBy('name')->get();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ride-form');
|
||||
}
|
||||
}
|
||||
92
app/Livewire/RideGalleryComponent.php
Normal file
92
app/Livewire/RideGalleryComponent.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use Livewire\Attributes\Rule;
|
||||
|
||||
class RideGalleryComponent extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
/** @var Ride */
|
||||
public Ride $ride;
|
||||
|
||||
/** @var \Illuminate\Http\UploadedFile */
|
||||
#[Rule('image|max:10240')] // 10MB Max
|
||||
public $photo;
|
||||
|
||||
/** @var string|null */
|
||||
public ?string $caption = null;
|
||||
|
||||
/** @var bool */
|
||||
public bool $showUploadForm = false;
|
||||
|
||||
public function mount(Ride $ride): void
|
||||
{
|
||||
$this->ride = $ride;
|
||||
}
|
||||
|
||||
public function toggleUploadForm(): void
|
||||
{
|
||||
$this->showUploadForm = !$this->showUploadForm;
|
||||
$this->reset(['photo', 'caption']);
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate([
|
||||
'photo' => 'required|image|max:10240',
|
||||
'caption' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$path = $this->photo->store('ride-photos', 'public');
|
||||
|
||||
$this->ride->photos()->create([
|
||||
'path' => $path,
|
||||
'caption' => $this->caption,
|
||||
'uploaded_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
$this->reset(['photo', 'caption']);
|
||||
$this->showUploadForm = false;
|
||||
|
||||
session()->flash('message', 'Photo uploaded successfully.');
|
||||
}
|
||||
|
||||
public function deletePhoto(int $photoId): void
|
||||
{
|
||||
$photo = $this->ride->photos()->findOrFail($photoId);
|
||||
|
||||
if ($photo->uploaded_by === Auth::id() || Gate::allows('delete-any-photo')) {
|
||||
Storage::disk('public')->delete($photo->path);
|
||||
$photo->delete();
|
||||
session()->flash('message', 'Photo deleted successfully.');
|
||||
} else {
|
||||
session()->flash('error', 'You do not have permission to delete this photo.');
|
||||
}
|
||||
}
|
||||
|
||||
public function setFeaturedPhoto(int $photoId): void
|
||||
{
|
||||
if (Gate::allows('edit', $this->ride)) {
|
||||
$this->ride->featured_photo_id = $photoId;
|
||||
$this->ride->save();
|
||||
session()->flash('message', 'Featured photo updated.');
|
||||
} else {
|
||||
session()->flash('error', 'You do not have permission to set the featured photo.');
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ride-gallery', [
|
||||
'photos' => $this->ride->photos()->latest()->paginate(12),
|
||||
]);
|
||||
}
|
||||
}
|
||||
101
app/Livewire/RideListComponent.php
Normal file
101
app/Livewire/RideListComponent.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Enums\RideCategory;
|
||||
use App\Models\Ride;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class RideListComponent extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
/** @var string */
|
||||
public $search = '';
|
||||
|
||||
/** @var string */
|
||||
public $category = '';
|
||||
|
||||
/** @var string */
|
||||
public $sortField = 'name';
|
||||
|
||||
/** @var string */
|
||||
public $sortDirection = 'asc';
|
||||
|
||||
/** @var string */
|
||||
public $viewMode = 'grid';
|
||||
|
||||
/**
|
||||
* Update search term and reset pagination.
|
||||
*/
|
||||
public function updatingSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update category filter and reset pagination.
|
||||
*/
|
||||
public function updatingCategory(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort results by the given field.
|
||||
*/
|
||||
public function sortBy(string $field): void
|
||||
{
|
||||
if ($this->sortField === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
|
||||
$this->sortField = $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between grid and list views.
|
||||
*/
|
||||
public function toggleView(): void
|
||||
{
|
||||
$this->viewMode = $this->viewMode === 'grid' ? 'list' : 'grid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available ride categories.
|
||||
*/
|
||||
public function getCategoriesProperty(): array
|
||||
{
|
||||
return RideCategory::options();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filtered and sorted rides query.
|
||||
*/
|
||||
private function getRidesQuery()
|
||||
{
|
||||
return Ride::query()
|
||||
->when($this->search, fn($query, $search) =>
|
||||
$query->where('name', 'like', "%{$search}%")
|
||||
)
|
||||
->when($this->category, fn($query, $category) =>
|
||||
$query->where('category', $category)
|
||||
)
|
||||
->orderBy($this->sortField, $this->sortDirection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component.
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$rides = $this->getRidesQuery()->paginate(12);
|
||||
|
||||
return view('livewire.ride-list', [
|
||||
'rides' => $rides,
|
||||
]);
|
||||
}
|
||||
}
|
||||
151
app/Livewire/RideReviewComponent.php
Normal file
151
app/Livewire/RideReviewComponent.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Review;
|
||||
use App\Models\Ride;
|
||||
use App\Enums\ReviewStatus;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Rule;
|
||||
|
||||
class RideReviewComponent extends Component
|
||||
{
|
||||
/**
|
||||
* The ride being reviewed
|
||||
*/
|
||||
public Ride $ride;
|
||||
|
||||
/**
|
||||
* The review being edited (if in edit mode)
|
||||
*/
|
||||
public ?Review $review = null;
|
||||
|
||||
/**
|
||||
* Form fields
|
||||
*/
|
||||
#[Rule('required|integer|min:1|max:5')]
|
||||
public int $rating = 3;
|
||||
|
||||
#[Rule('nullable|string|max:100')]
|
||||
public ?string $title = null;
|
||||
|
||||
#[Rule('required|string|min:10|max:2000')]
|
||||
public string $content = '';
|
||||
|
||||
/**
|
||||
* Whether the component is in edit mode
|
||||
*/
|
||||
public bool $isEditing = false;
|
||||
|
||||
/**
|
||||
* Success/error message
|
||||
*/
|
||||
public ?string $message = null;
|
||||
|
||||
/**
|
||||
* Mount the component
|
||||
*/
|
||||
public function mount(Ride $ride, ?Review $review = null)
|
||||
{
|
||||
$this->ride = $ride;
|
||||
|
||||
if ($review) {
|
||||
$this->review = $review;
|
||||
$this->isEditing = true;
|
||||
$this->rating = $review->rating;
|
||||
$this->title = $review->title;
|
||||
$this->content = $review->content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update the review
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
// Check if user is authenticated
|
||||
if (!Auth::check()) {
|
||||
$this->message = 'You must be logged in to submit a review.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
$key = 'review_' . Auth::id();
|
||||
if (RateLimiter::tooManyAttempts($key, 5)) { // 5 attempts per minute
|
||||
$this->message = 'Please wait before submitting another review.';
|
||||
return;
|
||||
}
|
||||
RateLimiter::hit($key);
|
||||
|
||||
// Validate input
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
if ($this->isEditing) {
|
||||
// Check if user can edit this review
|
||||
if (!$this->review || $this->review->user_id !== Auth::id()) {
|
||||
$this->message = 'You cannot edit this review.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update existing review
|
||||
$this->review->update([
|
||||
'rating' => $this->rating,
|
||||
'title' => $this->title,
|
||||
'content' => $this->content,
|
||||
'status' => ReviewStatus::PENDING,
|
||||
]);
|
||||
|
||||
$this->message = 'Review updated successfully. It will be visible after moderation.';
|
||||
} else {
|
||||
// Check if user already reviewed this ride
|
||||
if (!$this->ride->canBeReviewedBy(Auth::user())) {
|
||||
$this->message = 'You have already reviewed this ride.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new review
|
||||
Review::create([
|
||||
'ride_id' => $this->ride->id,
|
||||
'user_id' => Auth::id(),
|
||||
'rating' => $this->rating,
|
||||
'title' => $this->title,
|
||||
'content' => $this->content,
|
||||
'status' => ReviewStatus::PENDING,
|
||||
]);
|
||||
|
||||
$this->message = 'Review submitted successfully. It will be visible after moderation.';
|
||||
|
||||
// Reset form
|
||||
$this->reset(['rating', 'title', 'content']);
|
||||
}
|
||||
|
||||
$this->dispatch('review-saved');
|
||||
} catch (\Exception $e) {
|
||||
$this->message = 'An error occurred while saving your review. Please try again.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the form
|
||||
*/
|
||||
public function resetForm()
|
||||
{
|
||||
$this->reset(['rating', 'title', 'content', 'message']);
|
||||
if ($this->review) {
|
||||
$this->rating = $this->review->rating;
|
||||
$this->title = $this->review->title;
|
||||
$this->content = $this->review->content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ride-review-component');
|
||||
}
|
||||
}
|
||||
169
app/Livewire/RideReviewListComponent.php
Normal file
169
app/Livewire/RideReviewListComponent.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Review;
|
||||
use App\Models\Ride;
|
||||
use App\Models\HelpfulVote;
|
||||
use App\Enums\ReviewStatus;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class RideReviewListComponent extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
/**
|
||||
* The ride whose reviews are being displayed
|
||||
*/
|
||||
public Ride $ride;
|
||||
|
||||
/**
|
||||
* Current sort field
|
||||
*/
|
||||
public string $sortField = 'created_at';
|
||||
|
||||
/**
|
||||
* Current sort direction
|
||||
*/
|
||||
public string $sortDirection = 'desc';
|
||||
|
||||
/**
|
||||
* Rating filter
|
||||
*/
|
||||
public ?int $ratingFilter = null;
|
||||
|
||||
/**
|
||||
* Success/error message
|
||||
*/
|
||||
public ?string $message = null;
|
||||
|
||||
/**
|
||||
* Whether to show the statistics panel
|
||||
*/
|
||||
public bool $showStats = true;
|
||||
|
||||
/**
|
||||
* Listeners for events
|
||||
*/
|
||||
protected $listeners = [
|
||||
'review-saved' => '$refresh',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mount the component
|
||||
*/
|
||||
public function mount(Ride $ride)
|
||||
{
|
||||
$this->ride = $ride;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle sort field
|
||||
*/
|
||||
public function sortBy(string $field)
|
||||
{
|
||||
if ($this->sortField === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortField = $field;
|
||||
$this->sortDirection = 'desc';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by rating
|
||||
*/
|
||||
public function filterByRating(?int $rating)
|
||||
{
|
||||
$this->ratingFilter = $rating === $this->ratingFilter ? null : $rating;
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle helpful vote
|
||||
*/
|
||||
public function toggleHelpfulVote(Review $review)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
$this->message = 'You must be logged in to vote on reviews.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
$key = 'vote_' . Auth::id();
|
||||
if (RateLimiter::tooManyAttempts($key, 10)) { // 10 attempts per minute
|
||||
$this->message = 'Please wait before voting again.';
|
||||
return;
|
||||
}
|
||||
RateLimiter::hit($key);
|
||||
|
||||
try {
|
||||
HelpfulVote::toggle($review->id, Auth::id());
|
||||
$this->message = 'Vote recorded successfully.';
|
||||
} catch (\Exception $e) {
|
||||
$this->message = 'An error occurred while recording your vote.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle statistics panel
|
||||
*/
|
||||
public function toggleStats()
|
||||
{
|
||||
$this->showStats = !$this->showStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get review statistics
|
||||
*/
|
||||
public function getStatistics()
|
||||
{
|
||||
$reviews = $this->ride->reviews()->approved();
|
||||
|
||||
return [
|
||||
'total' => $reviews->count(),
|
||||
'average' => round($reviews->avg('rating'), 1),
|
||||
'distribution' => [
|
||||
5 => $reviews->where('rating', 5)->count(),
|
||||
4 => $reviews->where('rating', 4)->count(),
|
||||
3 => $reviews->where('rating', 3)->count(),
|
||||
2 => $reviews->where('rating', 2)->count(),
|
||||
1 => $reviews->where('rating', 1)->count(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reviews query
|
||||
*/
|
||||
protected function getReviewsQuery()
|
||||
{
|
||||
$query = $this->ride->reviews()
|
||||
->with(['user', 'helpfulVotes'])
|
||||
->approved();
|
||||
|
||||
// Apply rating filter
|
||||
if ($this->ratingFilter) {
|
||||
$query->where('rating', $this->ratingFilter);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
$query->orderBy($this->sortField, $this->sortDirection);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ride-review-list-component', [
|
||||
'reviews' => $this->getReviewsQuery()->paginate(10),
|
||||
'statistics' => $this->getStatistics(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
372
app/Livewire/RidesFilters.php
Normal file
372
app/Livewire/RidesFilters.php
Normal file
@@ -0,0 +1,372 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Models\Park;
|
||||
use App\Models\Operator;
|
||||
use App\Enums\RideCategory;
|
||||
use App\Enums\RideStatus;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Url;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class RidesFilters extends Component
|
||||
{
|
||||
// URL-bound filter properties for deep linking
|
||||
#[Url(as: 'category')]
|
||||
public ?string $selectedCategory = null;
|
||||
|
||||
#[Url(as: 'status')]
|
||||
public ?string $selectedStatus = null;
|
||||
|
||||
#[Url(as: 'manufacturer')]
|
||||
public ?int $selectedManufacturer = null;
|
||||
|
||||
#[Url(as: 'park')]
|
||||
public ?int $selectedPark = null;
|
||||
|
||||
#[Url(as: 'year_min')]
|
||||
public ?int $minOpeningYear = null;
|
||||
|
||||
#[Url(as: 'year_max')]
|
||||
public ?int $maxOpeningYear = null;
|
||||
|
||||
#[Url(as: 'height_min')]
|
||||
public ?int $minHeight = null;
|
||||
|
||||
#[Url(as: 'height_max')]
|
||||
public ?int $maxHeight = null;
|
||||
|
||||
// Filter options (cached)
|
||||
public array $categories = [];
|
||||
public array $statuses = [];
|
||||
public array $manufacturers = [];
|
||||
public array $parks = [];
|
||||
public array $yearRange = [];
|
||||
public array $heightRange = [];
|
||||
|
||||
// UI state
|
||||
public bool $showAdvancedFilters = false;
|
||||
public int $activeFiltersCount = 0;
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadFilterOptions();
|
||||
$this->calculateActiveFiltersCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load filter options with caching
|
||||
*/
|
||||
protected function loadFilterOptions(): void
|
||||
{
|
||||
// Categories from enum
|
||||
$this->categories = $this->remember(
|
||||
'categories',
|
||||
fn() => collect(RideCategory::cases())
|
||||
->map(fn($case) => [
|
||||
'value' => $case->value,
|
||||
'label' => $case->name,
|
||||
'count' => Ride::where('category', $case->value)->count()
|
||||
])
|
||||
->filter(fn($item) => $item['count'] > 0)
|
||||
->values()
|
||||
->toArray(),
|
||||
3600 // 1-hour cache
|
||||
);
|
||||
|
||||
// Statuses from enum
|
||||
$this->statuses = $this->remember(
|
||||
'statuses',
|
||||
fn() => collect(RideStatus::cases())
|
||||
->map(fn($case) => [
|
||||
'value' => $case->value,
|
||||
'label' => $case->name,
|
||||
'count' => Ride::where('status', $case->value)->count()
|
||||
])
|
||||
->filter(fn($item) => $item['count'] > 0)
|
||||
->values()
|
||||
->toArray(),
|
||||
3600
|
||||
);
|
||||
|
||||
// Manufacturers (Operators that have manufactured rides)
|
||||
$this->manufacturers = $this->remember(
|
||||
'manufacturers',
|
||||
fn() => Operator::select('id', 'name')
|
||||
->whereHas('manufacturedRides')
|
||||
->withCount('manufacturedRides')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(fn($operator) => [
|
||||
'value' => $operator->id,
|
||||
'label' => $operator->name,
|
||||
'count' => $operator->manufactured_rides_count
|
||||
])
|
||||
->toArray(),
|
||||
3600
|
||||
);
|
||||
|
||||
// Parks that have rides
|
||||
$this->parks = $this->remember(
|
||||
'parks',
|
||||
fn() => Park::select('id', 'name')
|
||||
->whereHas('rides')
|
||||
->withCount('rides')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(fn($park) => [
|
||||
'value' => $park->id,
|
||||
'label' => $park->name,
|
||||
'count' => $park->rides_count
|
||||
])
|
||||
->toArray(),
|
||||
3600
|
||||
);
|
||||
|
||||
// Year range
|
||||
$this->yearRange = $this->remember(
|
||||
'year_range',
|
||||
function() {
|
||||
$years = Ride::whereNotNull('opening_year')
|
||||
->selectRaw('MIN(opening_year) as min_year, MAX(opening_year) as max_year')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'min' => $years->min_year ?? 1900,
|
||||
'max' => $years->max_year ?? date('Y')
|
||||
];
|
||||
},
|
||||
3600
|
||||
);
|
||||
|
||||
// Height range
|
||||
$this->heightRange = $this->remember(
|
||||
'height_range',
|
||||
function() {
|
||||
$heights = Ride::whereNotNull('height_requirement')
|
||||
->selectRaw('MIN(height_requirement) as min_height, MAX(height_requirement) as max_height')
|
||||
->first();
|
||||
|
||||
return [
|
||||
'min' => $heights->min_height ?? 0,
|
||||
'max' => $heights->max_height ?? 200
|
||||
];
|
||||
},
|
||||
3600
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate number of active filters
|
||||
*/
|
||||
protected function calculateActiveFiltersCount(): void
|
||||
{
|
||||
$this->activeFiltersCount = collect([
|
||||
$this->selectedCategory,
|
||||
$this->selectedStatus,
|
||||
$this->selectedManufacturer,
|
||||
$this->selectedPark,
|
||||
$this->minOpeningYear,
|
||||
$this->maxOpeningYear,
|
||||
$this->minHeight,
|
||||
$this->maxHeight
|
||||
])->filter()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply category filter
|
||||
*/
|
||||
public function setCategory(?string $category): void
|
||||
{
|
||||
$this->selectedCategory = $category === $this->selectedCategory ? null : $category;
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply status filter
|
||||
*/
|
||||
public function setStatus(?string $status): void
|
||||
{
|
||||
$this->selectedStatus = $status === $this->selectedStatus ? null : $status;
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply manufacturer filter
|
||||
*/
|
||||
public function setManufacturer(?int $manufacturerId): void
|
||||
{
|
||||
$this->selectedManufacturer = $manufacturerId === $this->selectedManufacturer ? null : $manufacturerId;
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply park filter
|
||||
*/
|
||||
public function setPark(?int $parkId): void
|
||||
{
|
||||
$this->selectedPark = $parkId === $this->selectedPark ? null : $parkId;
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update year range filters
|
||||
*/
|
||||
public function updateYearRange(): void
|
||||
{
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update height range filters
|
||||
*/
|
||||
public function updateHeightRange(): void
|
||||
{
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle advanced filters visibility
|
||||
*/
|
||||
public function toggleAdvancedFilters(): void
|
||||
{
|
||||
$this->showAdvancedFilters = !$this->showAdvancedFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearAllFilters(): void
|
||||
{
|
||||
$this->selectedCategory = null;
|
||||
$this->selectedStatus = null;
|
||||
$this->selectedManufacturer = null;
|
||||
$this->selectedPark = null;
|
||||
$this->minOpeningYear = null;
|
||||
$this->maxOpeningYear = null;
|
||||
$this->minHeight = null;
|
||||
$this->maxHeight = null;
|
||||
|
||||
$this->calculateActiveFiltersCount();
|
||||
$this->dispatch('filters-updated', $this->getActiveFilters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active filters for parent component
|
||||
*/
|
||||
public function getActiveFilters(): array
|
||||
{
|
||||
return array_filter([
|
||||
'category' => $this->selectedCategory,
|
||||
'status' => $this->selectedStatus,
|
||||
'manufacturer_id' => $this->selectedManufacturer,
|
||||
'park_id' => $this->selectedPark,
|
||||
'min_opening_year' => $this->minOpeningYear,
|
||||
'max_opening_year' => $this->maxOpeningYear,
|
||||
'min_height' => $this->minHeight,
|
||||
'max_height' => $this->maxHeight,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filter summary for display
|
||||
*/
|
||||
public function getFilterSummary(): array
|
||||
{
|
||||
$summary = [];
|
||||
|
||||
if ($this->selectedCategory) {
|
||||
$category = collect($this->categories)->firstWhere('value', $this->selectedCategory);
|
||||
$summary[] = 'Category: ' . ($category['label'] ?? $this->selectedCategory);
|
||||
}
|
||||
|
||||
if ($this->selectedStatus) {
|
||||
$status = collect($this->statuses)->firstWhere('value', $this->selectedStatus);
|
||||
$summary[] = 'Status: ' . ($status['label'] ?? $this->selectedStatus);
|
||||
}
|
||||
|
||||
if ($this->selectedManufacturer) {
|
||||
$manufacturer = collect($this->manufacturers)->firstWhere('value', $this->selectedManufacturer);
|
||||
$summary[] = 'Manufacturer: ' . ($manufacturer['label'] ?? 'Unknown');
|
||||
}
|
||||
|
||||
if ($this->selectedPark) {
|
||||
$park = collect($this->parks)->firstWhere('value', $this->selectedPark);
|
||||
$summary[] = 'Park: ' . ($park['label'] ?? 'Unknown');
|
||||
}
|
||||
|
||||
if ($this->minOpeningYear || $this->maxOpeningYear) {
|
||||
$yearText = 'Year: ';
|
||||
if ($this->minOpeningYear && $this->maxOpeningYear) {
|
||||
$yearText .= $this->minOpeningYear . '-' . $this->maxOpeningYear;
|
||||
} elseif ($this->minOpeningYear) {
|
||||
$yearText .= 'After ' . $this->minOpeningYear;
|
||||
} else {
|
||||
$yearText .= 'Before ' . $this->maxOpeningYear;
|
||||
}
|
||||
$summary[] = $yearText;
|
||||
}
|
||||
|
||||
if ($this->minHeight || $this->maxHeight) {
|
||||
$heightText = 'Height: ';
|
||||
if ($this->minHeight && $this->maxHeight) {
|
||||
$heightText .= $this->minHeight . '-' . $this->maxHeight . 'cm';
|
||||
} elseif ($this->minHeight) {
|
||||
$heightText .= 'Min ' . $this->minHeight . 'cm';
|
||||
} else {
|
||||
$heightText .= 'Max ' . $this->maxHeight . 'cm';
|
||||
}
|
||||
$summary[] = $heightText;
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.rides-filters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
283
app/Livewire/RidesListing.php
Normal file
283
app/Livewire/RidesListing.php
Normal file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class RidesListing extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Search and filter properties with URL binding for deep linking
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'category')]
|
||||
public string $category = '';
|
||||
|
||||
#[Url(as: 'status')]
|
||||
public string $status = '';
|
||||
|
||||
#[Url(as: 'manufacturer')]
|
||||
public string $manufacturerId = '';
|
||||
|
||||
#[Url(as: 'year_from')]
|
||||
public string $openingYearFrom = '';
|
||||
|
||||
#[Url(as: 'year_to')]
|
||||
public string $openingYearTo = '';
|
||||
|
||||
#[Url(as: 'min_height')]
|
||||
public string $minHeight = '';
|
||||
|
||||
#[Url(as: 'max_height')]
|
||||
public string $maxHeight = '';
|
||||
|
||||
#[Url(as: 'park')]
|
||||
public string $parkId = '';
|
||||
|
||||
// Performance optimization
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'category' => ['except' => ''],
|
||||
'status' => ['except' => ''],
|
||||
'manufacturerId' => ['except' => ''],
|
||||
'openingYearFrom' => ['except' => ''],
|
||||
'openingYearTo' => ['except' => ''],
|
||||
'minHeight' => ['except' => ''],
|
||||
'maxHeight' => ['except' => ''],
|
||||
'parkId' => ['except' => ''],
|
||||
'page' => ['except' => 1],
|
||||
];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
// Initialize component state
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset pagination when search/filters change
|
||||
*/
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('rides');
|
||||
}
|
||||
|
||||
public function updatedCategory(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('rides');
|
||||
}
|
||||
|
||||
public function updatedStatus(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('rides');
|
||||
}
|
||||
|
||||
public function updatedManufacturerId(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('rides');
|
||||
}
|
||||
|
||||
public function updatedOpeningYearFrom(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('rides');
|
||||
}
|
||||
|
||||
public function updatedOpeningYearTo(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('rides');
|
||||
}
|
||||
|
||||
public function updatedMinHeight(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('rides');
|
||||
}
|
||||
|
||||
public function updatedMaxHeight(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('rides');
|
||||
}
|
||||
|
||||
public function updatedParkId(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('rides');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset([
|
||||
'search',
|
||||
'category',
|
||||
'status',
|
||||
'manufacturerId',
|
||||
'openingYearFrom',
|
||||
'openingYearTo',
|
||||
'minHeight',
|
||||
'maxHeight',
|
||||
'parkId'
|
||||
]);
|
||||
$this->resetPage();
|
||||
$this->invalidateCache('rides');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rides with Django parity search and filtering
|
||||
*/
|
||||
public function getRidesProperty()
|
||||
{
|
||||
$cacheKey = $this->getCacheKey('rides.' . md5(serialize([
|
||||
'search' => $this->search,
|
||||
'category' => $this->category,
|
||||
'status' => $this->status,
|
||||
'manufacturerId' => $this->manufacturerId,
|
||||
'openingYearFrom' => $this->openingYearFrom,
|
||||
'openingYearTo' => $this->openingYearTo,
|
||||
'minHeight' => $this->minHeight,
|
||||
'maxHeight' => $this->maxHeight,
|
||||
'parkId' => $this->parkId,
|
||||
'page' => $this->getPage(),
|
||||
])));
|
||||
|
||||
return $this->remember($cacheKey, function () {
|
||||
return $this->buildQuery()->paginate(12);
|
||||
}, 300); // 5 minute cache
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the query with Django parity search and filters
|
||||
*/
|
||||
protected function buildQuery()
|
||||
{
|
||||
$query = Ride::query()
|
||||
->with(['park', 'manufacturer', 'designer', 'photos' => function ($q) {
|
||||
$q->where('is_featured', true)->limit(1);
|
||||
}]);
|
||||
|
||||
// Django parity multi-term search
|
||||
if (!empty($this->search)) {
|
||||
$terms = explode(' ', trim($this->search));
|
||||
foreach ($terms as $term) {
|
||||
$query->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhereHas('park', fn($q) => $q->where('name', 'ilike', "%{$term}%"))
|
||||
->orWhereHas('manufacturer', fn($q) => $q->where('name', 'ilike', "%{$term}%"))
|
||||
->orWhereHas('designer', fn($q) => $q->where('name', 'ilike', "%{$term}%"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filters with Django parity
|
||||
$query = $this->applyFilters($query);
|
||||
|
||||
return $query->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters with Django parity
|
||||
*/
|
||||
protected function applyFilters($query)
|
||||
{
|
||||
return $query
|
||||
->when($this->category, fn($q, $category) =>
|
||||
$q->where('ride_type', $category))
|
||||
->when($this->status, fn($q, $status) =>
|
||||
$q->where('status', $status))
|
||||
->when($this->manufacturerId, fn($q, $manufacturerId) =>
|
||||
$q->where('manufacturer_id', $manufacturerId))
|
||||
->when($this->openingYearFrom, fn($q, $year) =>
|
||||
$q->where('opening_date', '>=', "{$year}-01-01"))
|
||||
->when($this->openingYearTo, fn($q, $year) =>
|
||||
$q->where('opening_date', '<=', "{$year}-12-31"))
|
||||
->when($this->minHeight, fn($q, $height) =>
|
||||
$q->where('height_requirement', '>=', $height))
|
||||
->when($this->maxHeight, fn($q, $height) =>
|
||||
$q->where('height_requirement', '<=', $height))
|
||||
->when($this->parkId, fn($q, $parkId) =>
|
||||
$q->where('park_id', $parkId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available filter options for dropdowns
|
||||
*/
|
||||
public function getFilterOptionsProperty()
|
||||
{
|
||||
return $this->remember('filter_options', function () {
|
||||
return [
|
||||
'categories' => Ride::select('ride_type')
|
||||
->distinct()
|
||||
->whereNotNull('ride_type')
|
||||
->orderBy('ride_type')
|
||||
->pluck('ride_type', 'ride_type'),
|
||||
'statuses' => Ride::select('status')
|
||||
->distinct()
|
||||
->whereNotNull('status')
|
||||
->orderBy('status')
|
||||
->pluck('status', 'status'),
|
||||
'manufacturers' => \App\Models\Manufacturer::orderBy('name')
|
||||
->pluck('name', 'id'),
|
||||
'parks' => \App\Models\Park::orderBy('name')
|
||||
->pluck('name', 'id'),
|
||||
];
|
||||
}, 3600); // 1 hour cache for filter options
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.rides-listing', [
|
||||
'rides' => $this->rides,
|
||||
'filterOptions' => $this->filterOptions,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
158
app/Livewire/RidesListingUniversal.php
Normal file
158
app/Livewire/RidesListingUniversal.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class RidesListingUniversal extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Universal Listing System integration
|
||||
public string $entityType = 'rides';
|
||||
|
||||
// Search and filter properties with URL binding
|
||||
#[Url(as: 'q')]
|
||||
public string $search = '';
|
||||
|
||||
#[Url(as: 'categories')]
|
||||
public array $categories = [];
|
||||
|
||||
#[Url(as: 'opening_year_from')]
|
||||
public string $openingYearFrom = '';
|
||||
|
||||
#[Url(as: 'opening_year_to')]
|
||||
public string $openingYearTo = '';
|
||||
|
||||
#[Url(as: 'sort')]
|
||||
public string $sortBy = 'name';
|
||||
|
||||
#[Url(as: 'view')]
|
||||
public string $viewMode = 'grid';
|
||||
|
||||
/**
|
||||
* Get rides data for Universal Listing System
|
||||
*/
|
||||
public function getRidesProperty()
|
||||
{
|
||||
$cacheKey = 'rides_listing_' . md5(serialize([
|
||||
'search' => $this->search,
|
||||
'categories' => $this->categories,
|
||||
'openingYearFrom' => $this->openingYearFrom,
|
||||
'openingYearTo' => $this->openingYearTo,
|
||||
'sortBy' => $this->sortBy,
|
||||
'page' => $this->getPage(),
|
||||
]));
|
||||
|
||||
return Cache::remember($cacheKey, 300, function () {
|
||||
return $this->buildQuery()->paginate(12);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the optimized query
|
||||
*/
|
||||
protected function buildQuery()
|
||||
{
|
||||
$query = Ride::query()
|
||||
->with(['park', 'manufacturer', 'designer', 'photos' => function ($q) {
|
||||
$q->where('is_featured', true)->limit(1);
|
||||
}]);
|
||||
|
||||
// Multi-term search with Django parity
|
||||
if (!empty($this->search)) {
|
||||
$terms = explode(' ', trim($this->search));
|
||||
foreach ($terms as $term) {
|
||||
$query->where(function ($subQuery) use ($term) {
|
||||
$subQuery->where('name', 'ilike', "%{$term}%")
|
||||
->orWhere('description', 'ilike', "%{$term}%")
|
||||
->orWhereHas('park', fn($q) => $q->where('name', 'ilike', "%{$term}%"))
|
||||
->orWhereHas('manufacturer', fn($q) => $q->where('name', 'ilike', "%{$term}%"))
|
||||
->orWhereHas('designer', fn($q) => $q->where('name', 'ilike', "%{$term}%"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if (!empty($this->categories)) {
|
||||
$query->whereIn('ride_type', $this->categories);
|
||||
}
|
||||
|
||||
if (!empty($this->openingYearFrom)) {
|
||||
$query->where('opening_date', '>=', "{$this->openingYearFrom}-01-01");
|
||||
}
|
||||
|
||||
if (!empty($this->openingYearTo)) {
|
||||
$query->where('opening_date', '<=', "{$this->openingYearTo}-12-31");
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
switch ($this->sortBy) {
|
||||
case 'opening_year':
|
||||
$query->orderBy('opening_date', 'desc');
|
||||
break;
|
||||
case 'thrill_rating':
|
||||
$query->orderBy('thrill_rating', 'desc');
|
||||
break;
|
||||
case 'height_meters':
|
||||
$query->orderBy('height_meters', 'desc');
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('name');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset pagination when filters change
|
||||
*/
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedCategories(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedOpeningYearFrom(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedOpeningYearTo(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatedSortBy(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset(['search', 'categories', 'openingYearFrom', 'openingYearTo']);
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component using Universal Listing System
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.rides-listing-universal', [
|
||||
'items' => $this->rides,
|
||||
'entityType' => $this->entityType,
|
||||
]);
|
||||
}
|
||||
}
|
||||
221
app/Livewire/RidesSearchSuggestions.php
Normal file
221
app/Livewire/RidesSearchSuggestions.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Ride;
|
||||
use App\Models\Park;
|
||||
use App\Models\Operator;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\On;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class RidesSearchSuggestions extends Component
|
||||
{
|
||||
public string $query = '';
|
||||
public bool $showSuggestions = false;
|
||||
public int $maxSuggestions = 8;
|
||||
public array $suggestions = [];
|
||||
|
||||
/**
|
||||
* Component initialization
|
||||
*/
|
||||
public function mount(string $query = ''): void
|
||||
{
|
||||
$this->query = $query;
|
||||
if (!empty($query)) {
|
||||
$this->updateSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for search query updates from parent components
|
||||
*/
|
||||
#[On('search-query-updated')]
|
||||
public function handleSearchUpdate(string $query): void
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->updateSuggestions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update search suggestions based on current query
|
||||
*/
|
||||
public function updateSuggestions(): void
|
||||
{
|
||||
if (strlen($this->query) < 2) {
|
||||
$this->suggestions = [];
|
||||
$this->showSuggestions = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->suggestions = $this->remember(
|
||||
'suggestions.' . md5(strtolower($this->query)),
|
||||
fn() => $this->buildSuggestions(),
|
||||
300 // 5-minute cache for suggestions
|
||||
);
|
||||
|
||||
$this->showSuggestions = !empty($this->suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build search suggestions from multiple sources
|
||||
*/
|
||||
protected function buildSuggestions(): array
|
||||
{
|
||||
$query = strtolower(trim($this->query));
|
||||
$suggestions = collect();
|
||||
|
||||
// Ride name suggestions
|
||||
$rideSuggestions = Ride::select('name', 'slug', 'id')
|
||||
->with(['park:id,name,slug'])
|
||||
->where('name', 'ilike', "%{$query}%")
|
||||
->limit(4)
|
||||
->get()
|
||||
->map(function ($ride) {
|
||||
return [
|
||||
'type' => 'ride',
|
||||
'title' => $ride->name,
|
||||
'subtitle' => $ride->park->name ?? 'Unknown Park',
|
||||
'url' => route('rides.show', $ride->slug),
|
||||
'icon' => 'ride',
|
||||
'category' => 'Rides'
|
||||
];
|
||||
});
|
||||
|
||||
// Park name suggestions
|
||||
$parkSuggestions = Park::select('name', 'slug', 'id')
|
||||
->where('name', 'ilike', "%{$query}%")
|
||||
->limit(3)
|
||||
->get()
|
||||
->map(function ($park) {
|
||||
return [
|
||||
'type' => 'park',
|
||||
'title' => $park->name,
|
||||
'subtitle' => 'Theme Park',
|
||||
'url' => route('parks.show', $park->slug),
|
||||
'icon' => 'park',
|
||||
'category' => 'Parks'
|
||||
];
|
||||
});
|
||||
|
||||
// Manufacturer/Designer suggestions
|
||||
$operatorSuggestions = Operator::select('name', 'slug', 'id')
|
||||
->where('name', 'ilike', "%{$query}%")
|
||||
->limit(2)
|
||||
->get()
|
||||
->map(function ($operator) {
|
||||
return [
|
||||
'type' => 'operator',
|
||||
'title' => $operator->name,
|
||||
'subtitle' => 'Manufacturer/Designer',
|
||||
'url' => route('operators.show', $operator->slug),
|
||||
'icon' => 'operator',
|
||||
'category' => 'Companies'
|
||||
];
|
||||
});
|
||||
|
||||
// Combine and prioritize suggestions
|
||||
$suggestions = $suggestions
|
||||
->concat($rideSuggestions)
|
||||
->concat($parkSuggestions)
|
||||
->concat($operatorSuggestions)
|
||||
->take($this->maxSuggestions);
|
||||
|
||||
return $suggestions->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle suggestion selection
|
||||
*/
|
||||
public function selectSuggestion(array $suggestion): void
|
||||
{
|
||||
$this->dispatch('suggestion-selected', $suggestion);
|
||||
$this->hideSuggestions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide suggestions dropdown
|
||||
*/
|
||||
public function hideSuggestions(): void
|
||||
{
|
||||
$this->showSuggestions = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show suggestions dropdown
|
||||
*/
|
||||
public function showSuggestionsDropdown(): void
|
||||
{
|
||||
if (!empty($this->suggestions)) {
|
||||
$this->showSuggestions = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input focus
|
||||
*/
|
||||
public function onFocus(): void
|
||||
{
|
||||
$this->showSuggestionsDropdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle input blur with delay to allow clicks
|
||||
*/
|
||||
public function onBlur(): void
|
||||
{
|
||||
// Delay hiding to allow suggestion clicks
|
||||
$this->dispatch('delayed-hide-suggestions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon class for suggestion type
|
||||
*/
|
||||
public function getIconClass(string $type): string
|
||||
{
|
||||
return match($type) {
|
||||
'ride' => 'fas fa-roller-coaster',
|
||||
'park' => 'fas fa-map-marker-alt',
|
||||
'operator' => 'fas fa-industry',
|
||||
default => 'fas fa-search'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.rides-search-suggestions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for this component
|
||||
*/
|
||||
protected function getCacheKey(string $suffix = ''): string
|
||||
{
|
||||
return 'thrillwiki.' . class_basename(static::class) . '.' . $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember data with caching
|
||||
*/
|
||||
protected function remember(string $key, $callback, int $ttl = 3600)
|
||||
{
|
||||
return Cache::remember($this->getCacheKey($key), $ttl, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate component cache
|
||||
*/
|
||||
protected function invalidateCache(string $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
Cache::forget($this->getCacheKey($key));
|
||||
} else {
|
||||
// Clear all cache for this component
|
||||
Cache::flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
148
app/Livewire/SearchComponent.php
Normal file
148
app/Livewire/SearchComponent.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Park;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class SearchComponent extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
// Filter properties
|
||||
public string $search = '';
|
||||
public string $location = '';
|
||||
public ?float $minRating = null;
|
||||
public ?float $maxRating = null;
|
||||
public ?int $minRides = null;
|
||||
public ?int $minCoasters = null;
|
||||
public bool $filtersApplied = false;
|
||||
|
||||
// Querystring parameters
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'location' => ['except' => ''],
|
||||
'minRating' => ['except' => ''],
|
||||
'maxRating' => ['except' => ''],
|
||||
'minRides' => ['except' => ''],
|
||||
'minCoasters' => ['except' => '']
|
||||
];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->filtersApplied = $this->hasActiveFilters();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.search', [
|
||||
'results' => $this->getFilteredParks()
|
||||
]);
|
||||
}
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->filtersApplied = $this->hasActiveFilters();
|
||||
}
|
||||
|
||||
#[On('suggestion-selected')]
|
||||
public function handleSuggestionSelected($id, $text): void
|
||||
{
|
||||
$park = Park::find($id);
|
||||
if ($park) {
|
||||
$this->search = $text;
|
||||
$this->filtersApplied = $this->hasActiveFilters();
|
||||
redirect()->route('parks.show', $park);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedLocation(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->filtersApplied = $this->hasActiveFilters();
|
||||
}
|
||||
|
||||
public function updatedMinRating(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->filtersApplied = $this->hasActiveFilters();
|
||||
}
|
||||
|
||||
public function updatedMaxRating(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->filtersApplied = $this->hasActiveFilters();
|
||||
}
|
||||
|
||||
public function updatedMinRides(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->filtersApplied = $this->hasActiveFilters();
|
||||
}
|
||||
|
||||
public function updatedMinCoasters(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
$this->filtersApplied = $this->hasActiveFilters();
|
||||
}
|
||||
|
||||
public function clearFilters(): void
|
||||
{
|
||||
$this->reset([
|
||||
'search',
|
||||
'location',
|
||||
'minRating',
|
||||
'maxRating',
|
||||
'minRides',
|
||||
'minCoasters'
|
||||
]);
|
||||
$this->filtersApplied = false;
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
protected function getFilteredParks()
|
||||
{
|
||||
return Park::query()
|
||||
->select('parks.*')
|
||||
->with(['location', 'photos'])
|
||||
->when($this->search, function (Builder $query) {
|
||||
$query->where(function (Builder $query) {
|
||||
$query->where('name', 'like', "%{$this->search}%")
|
||||
->orWhere('description', 'like', "%{$this->search}%");
|
||||
});
|
||||
})
|
||||
->when($this->location, function (Builder $query) {
|
||||
$query->whereHas('location', function (Builder $query) {
|
||||
$query->where('address_text', 'like', "%{$this->location}%");
|
||||
});
|
||||
})
|
||||
->when($this->minRating, function (Builder $query) {
|
||||
$query->where('average_rating', '>=', $this->minRating);
|
||||
})
|
||||
->when($this->maxRating, function (Builder $query) {
|
||||
$query->where('average_rating', '<=', $this->maxRating);
|
||||
})
|
||||
->when($this->minRides, function (Builder $query) {
|
||||
$query->where('ride_count', '>=', $this->minRides);
|
||||
})
|
||||
->when($this->minCoasters, function (Builder $query) {
|
||||
$query->where('coaster_count', '>=', $this->minCoasters);
|
||||
})
|
||||
->paginate(10);
|
||||
}
|
||||
|
||||
protected function hasActiveFilters(): bool
|
||||
{
|
||||
return !empty($this->search)
|
||||
|| !empty($this->location)
|
||||
|| !empty($this->minRating)
|
||||
|| !empty($this->maxRating)
|
||||
|| !empty($this->minRides)
|
||||
|| !empty($this->minCoasters);
|
||||
}
|
||||
}
|
||||
27
app/Livewire/ThemeToggleComponent.php
Normal file
27
app/Livewire/ThemeToggleComponent.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class ThemeToggleComponent extends Component
|
||||
{
|
||||
public bool $isDark = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->isDark = session('theme') === 'dark';
|
||||
}
|
||||
|
||||
public function toggleTheme()
|
||||
{
|
||||
$this->isDark = !$this->isDark;
|
||||
session(['theme' => $this->isDark ? 'dark' : 'light']);
|
||||
$this->dispatch('theme-changed', theme: $this->isDark ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.theme-toggle-component');
|
||||
}
|
||||
}
|
||||
25
app/Livewire/UserMenuComponent.php
Normal file
25
app/Livewire/UserMenuComponent.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class UserMenuComponent extends Component
|
||||
{
|
||||
public bool $isOpen = false;
|
||||
|
||||
public function toggle()
|
||||
{
|
||||
$this->isOpen = !$this->isOpen;
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
$this->isOpen = false;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.user-menu-component');
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user