Compare commits

...

3 Commits

Author SHA1 Message Date
pacnpal
a57e5deb3f feat: implement search functionality with real-time filtering and pagination 2025-02-25 22:14:13 -05:00
pacnpal
5d2908127b add memorybank 2025-02-25 21:59:44 -05:00
pacnpal
487c0e5866 feat: implement ride review components
- Add RideReviewComponent for submitting reviews
  - Star rating input with real-time validation
  - Rate limiting and anti-spam measures
  - Edit capabilities for own reviews

- Add RideReviewListComponent for displaying reviews
  - Paginated list with sort/filter options
  - Helpful vote functionality
  - Statistics display with rating distribution

- Add ReviewModerationComponent for review management
  - Review queue with status filters
  - Approve/reject functionality
  - Batch actions support
  - Edit capabilities

- Update Memory Bank documentation
  - Document component implementations
  - Track feature completion
  - Update technical decisions
2025-02-25 21:59:22 -05:00
18 changed files with 2831 additions and 51 deletions

245
.clinerules-architect Normal file
View 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
View 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
View 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.'

14
.roomodes Normal file
View 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"
}
]
}

View 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(),
]);
}
}

View 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');
}
}

View 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(),
]);
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace App\Livewire;
use App\Models\Park;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
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();
}
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);
}
}

View File

@@ -1,12 +1,30 @@
# Active Development Context
## Current Task
Migrating the design from Django to Laravel implementation
Implementing search functionality (✅ Completed)
## Recent Changes
1. Implemented search functionality:
- ✅ Created SearchComponent with real-time filtering
- ✅ Implemented responsive search UI with filters sidebar
- ✅ Added park cards with dynamic content
- ✅ Integrated dark mode support
- ✅ Added pagination and URL state management
- ✅ Created comprehensive documentation in SearchImplementation.md
## Progress Summary
### Completed Tasks
1. Static Assets Migration
1. Search Implementation
- Created SearchComponent with real-time filtering
- Implemented responsive search UI with filters sidebar
- Added park cards with dynamic content
- Integrated dark mode support
- Added pagination and URL state management
- Created comprehensive documentation
- See `memory-bank/features/SearchImplementation.md` for details
2. Static Assets Migration
- Created directory structure for images, CSS, and JavaScript
- Copied images from Django project
- Migrated JavaScript modules
@@ -50,8 +68,23 @@ Migrating the design from Django to Laravel implementation
- User menu
- Auth menu
- Park list with filtering and view modes
- Search with real-time filtering
- Review system with moderation
- Ride management and details
### Next Steps
1. Filament Admin Implementation
- Create admin panel for parks management
- Implement CRUD operations using Filament resources
- Set up role-based access control
- Add audit trails for admin actions
- See `memory-bank/features/FilamentIntegration.md` for details
2. Analytics Integration
- Implement analytics tracking
- Create statistics dashboard
- Add reporting features
- Set up data aggregation
1. ✅ Park Model Enhancements
- ✅ Implemented Photo model and relationship
- ✅ Added getBySlug method for historical slug support
@@ -122,11 +155,23 @@ Migrating the design from Django to Laravel implementation
- ✅ HelpfulVote model with toggle functionality (app/Models/HelpfulVote.php)
- ✅ Added review relationships to Ride model (app/Models/Ride.php)
- ✅ See `memory-bank/models/ReviewModels.md` for documentation
- Implement Livewire components:
- RideReviewComponent for submitting reviews
- RideReviewListComponent for displaying reviews
- ReviewModerationComponent for moderators
- See `memory-bank/features/RideReviews.md` for implementation details
- Implement Livewire components:
- RideReviewComponent for submitting reviews
- ✅ Form with star rating input
- ✅ Real-time validation
- ✅ Rate limiting
- ✅ Edit capabilities
- ✅ RideReviewListComponent for displaying reviews
- ✅ Paginated list view
- ✅ Sort and filter options
- ✅ Helpful vote system
- ✅ Statistics display
- ✅ ReviewModerationComponent for moderators
- ✅ Review queue with filters
- ✅ Approve/reject functionality
- ✅ Batch actions
- ✅ Edit capabilities
- ✅ See `memory-bank/features/RideReviews.md` for implementation details
- Implement views and templates:
- ✅ Ride list page (resources/views/livewire/ride-list.blade.php)
- ✅ Ride create/edit form (resources/views/livewire/ride-form.blade.php)

View File

@@ -0,0 +1,212 @@
# Review System Livewire Components
## Overview
The review system consists of three main Livewire components that handle the creation, display, and moderation of ride reviews. These components maintain feature parity with the Django implementation while leveraging Laravel and Livewire's reactive capabilities.
### RideReviewComponent
#### Overview
The RideReviewComponent provides a form interface for users to submit reviews for rides, with real-time validation and anti-spam measures.
**Location**:
- Component: `app/Livewire/RideReviewComponent.php`
- View: `resources/views/livewire/ride-review.blade.php`
#### Features
- Star rating input (1-5)
- Optional title field
- Required content field
- Real-time validation
- Anti-spam protection
- Success/error messaging
- One review per ride enforcement
- Edit capabilities for own reviews
#### Implementation Details
1. **Form Handling**
- Real-time validation using Livewire
- Star rating widget implementation
- Character count tracking
- Form state management
- Edit mode support
2. **Security Features**
- Rate limiting
- Duplicate prevention
- Permission checks
- Input sanitization
- CSRF protection
3. **UI Components**
- Star rating selector
- Dynamic character counter
- Form validation feedback
- Success/error alerts
- Loading states
4. **Business Logic**
- Review uniqueness check
- Permission verification
- Status management
- Edit history tracking
### RideReviewListComponent
#### Overview
The RideReviewListComponent displays a paginated list of reviews with sorting, filtering, and helpful vote functionality.
**Location**:
- Component: `app/Livewire/RideReviewListComponent.php`
- View: `resources/views/livewire/ride-review-list.blade.php`
#### Features
- Grid/list view toggle
- Pagination support
- Sort by date/rating
- Filter by rating
- Helpful vote system
- Review statistics display
- Responsive design
#### Implementation Details
1. **List Management**
- Pagination handling
- Sort state management
- Filter application
- Dynamic loading
2. **Vote System**
- Helpful vote toggle
- Vote count tracking
- User vote status
- Rate limiting
3. **UI Components**
- Review cards/rows
- Sort/filter controls
- Pagination links
- Statistics summary
- Loading states
4. **Statistics Display**
- Average rating
- Rating distribution
- Review count
- Helpful vote tallies
### ReviewModerationComponent
#### Overview
The ReviewModerationComponent provides an interface for moderators to review, approve, reject, and edit user reviews.
**Location**:
- Component: `app/Livewire/ReviewModerationComponent.php`
- View: `resources/views/livewire/review-moderation.blade.php`
#### Features
- Review queue display
- Approve/reject actions
- Edit capabilities
- Status tracking
- Moderation history
- Batch actions
- Search/filter
#### Implementation Details
1. **Queue Management**
- Status-based filtering
- Priority sorting
- Batch processing
- History tracking
2. **Moderation Actions**
- Approval workflow
- Rejection handling
- Edit interface
- Status updates
- Notification system
3. **UI Components**
- Queue display
- Action buttons
- Edit forms
- Status indicators
- History timeline
4. **Security Features**
- Role verification
- Action logging
- Permission checks
- Edit tracking
## Integration Points
### With RideDetailComponent
- Review form placement
- Review list integration
- Statistics display
- Component communication
### With User System
- Permission checks
- User identification
- Rate limiting
- Profile integration
### With Notification System
- Review notifications
- Moderation alerts
- Status updates
- User feedback
## Technical Decisions
1. **Real-time Validation**
- Using Livewire's real-time validation for immediate feedback
- Client-side validation for better UX
- Server-side validation for security
2. **State Management**
- Component properties for form state
- Session for moderation queue
- Cache for statistics
- Database for permanent storage
3. **Performance Optimization**
- Eager loading relationships
- Caching review counts
- Lazy loading images
- Pagination implementation
4. **Security Measures**
- Rate limiting implementation
- Input sanitization
- Permission checks
- CSRF protection
- XSS prevention
## Testing Strategy
1. **Unit Tests**
- Component methods
- Validation rules
- Business logic
- Helper functions
2. **Feature Tests**
- Form submission
- Validation handling
- Moderation flow
- Vote system
3. **Integration Tests**
- Component interaction
- Event handling
- State management
- Error handling
4. **Browser Tests**
- UI interactions
- Real-time updates
- Responsive design
- JavaScript integration

View File

@@ -24,57 +24,69 @@ The ride reviews system allows users to rate and review rides, providing both nu
- user_id (foreign key to users)
- created_at (timestamp)
## Components to Implement
## Components Implemented
### RideReviewComponent
- Display review form
- Handle review submission
- Validate input
- Show success/error messages
- Display review form
- Handle review submission
- Validate input
- Show success/error messages
- Rate limiting implemented ✅
- One review per ride enforcement ✅
- Edit capabilities ✅
### RideReviewListComponent
- Display reviews for a ride
- Pagination support
- Sorting options
- Helpful vote functionality
- Filter options (rating, date)
- Display reviews for a ride
- Pagination support
- Sorting options
- Helpful vote functionality
- Filter options (rating, date)
- Statistics display ✅
- Dark mode support ✅
### ReviewModerationComponent
- Review queue for moderators
- Approve/reject functionality
- Edit capabilities
- Status tracking
- Review queue for moderators
- Approve/reject functionality
- Edit capabilities
- Status tracking
- Batch actions ✅
- Search functionality ✅
## Features Required
## Features Implemented
1. Review Creation
1. Review Creation
- Rating input (1-5 stars)
- Title field (optional)
- Content field
- Client & server validation
- Anti-spam measures
- Rate limiting
2. Review Display
2. Review Display
- List/grid view of reviews
- Sorting by date/rating
- Pagination
- Rating statistics
- Helpful vote system
- Dark mode support
3. Moderation System
3. Moderation System
- Review queue
- Approval workflow
- Edit capabilities
- Status management
- Moderation history
- Batch actions
- Search functionality
4. User Features
4. User Features
- One review per ride per user
- Edit own reviews
- Delete own reviews
- Vote on helpful reviews
- Rate limiting on votes
5. Statistics
5. Statistics
- Average rating calculation
- Rating distribution
- Review count tracking
@@ -95,39 +107,39 @@ The ride reviews system allows users to rate and review rides, providing both nu
- ✅ Created ReviewStatus enum (app/Enums/ReviewStatus.php)
- ✅ Implemented methods for average rating and review counts
3. Components
- Review form component
- Review list component
- Moderation component
- Statistics display
3. Components
- Review form component
- Review list component
- Moderation component
- Statistics display
4. Business Logic
- Rating calculations
- Permission checks
- Validation rules
- Anti-spam measures
4. Business Logic
- Rating calculations
- Permission checks
- Validation rules
- Anti-spam measures
5. Testing
- Unit tests
- Feature tests
- Integration tests
- User flow testing
- Unit tests (TODO)
- Feature tests (TODO)
- Integration tests (TODO)
- User flow testing (TODO)
## Security Considerations
1. Authorization
1. Authorization
- User authentication required
- Rate limiting
- Rate limiting implemented
- Moderation permissions
- Edit/delete permissions
2. Data Validation
2. Data Validation
- Input sanitization
- Rating range validation
- Content length limits
- Duplicate prevention
3. Anti-Abuse
3. Anti-Abuse
- Rate limiting
- Spam detection
- Vote manipulation prevention
@@ -137,8 +149,6 @@ The ride reviews system allows users to rate and review rides, providing both nu
### Model Implementation
The review system consists of two main models:
1. Review - Represents a user's review of a ride
- Implemented in `app/Models/Review.php`
- Uses ReviewStatus enum for status management
@@ -158,4 +168,30 @@ The review system consists of two main models:
- Created canBeReviewedBy method to check if a user can review a ride
- Implemented addReview method for creating new reviews
These models follow Laravel's Eloquent ORM patterns while maintaining feature parity with the Django implementation.
### Component Implementation
1. RideReviewComponent
- Form-based component for submitting reviews
- Real-time validation using Livewire
- Rate limiting using Laravel's RateLimiter
- Edit mode support for updating reviews
- Success/error message handling
- Dark mode support
2. RideReviewListComponent
- Paginated list of reviews
- Sort by date or rating
- Filter by rating
- Helpful vote functionality
- Statistics panel with rating distribution
- Dark mode support
3. ReviewModerationComponent
- Queue-based moderation interface
- Status-based filtering (pending, approved, rejected)
- Search functionality
- Batch actions for approve/reject
- Edit modal for review modification
- Dark mode support
These components follow Laravel's Eloquent ORM patterns while maintaining feature parity with the Django implementation. The use of Livewire enables real-time interactivity without requiring custom JavaScript.

View File

@@ -0,0 +1,139 @@
# Search Implementation
## Overview
The search functionality has been migrated from Django to Laravel/Livewire while maintaining feature parity and improving the user experience with real-time filtering.
## Key Components
### SearchComponent (app/Livewire/SearchComponent.php)
- Handles search and filtering logic
- Uses Livewire's real-time search capabilities
- Maintains query parameters in URL
- Implements pagination for results
#### Filter Properties
- `search`: Text search across name and description
- `location`: Location-based filtering
- `minRating` and `maxRating`: Rating range filtering
- `minRides`: Minimum number of rides filter
- `minCoasters`: Minimum number of coasters filter
#### Features
- Real-time filtering with `wire:model.live`
- URL query string synchronization
- Eager loading of relationships for performance
- Responsive pagination
- Filter state management
### View Implementation (resources/views/livewire/search.blade.php)
- Responsive layout with filters sidebar
- Real-time updates without page reload
- Dark mode support
- Accessible form controls
- Mobile-first design
#### UI Components
1. Filters Sidebar
- Search input
- Location filter
- Rating range inputs
- Ride count filters
- Clear filters button
2. Results Section
- Results count display
- Park cards with:
* Featured image
* Park name and location
* Rating badge
* Status indicator
* Ride/coaster counts
* Description preview
## Differences from Django Implementation
### Improvements
1. Real-time Updates
- Replaced HTMX with Livewire's native reactivity
- Instant filtering without page reloads
- Smoother user experience
2. State Management
- URL query parameters for shareable searches
- Persistent filter state during navigation
- Clear filters functionality
3. Performance
- Eager loading of relationships
- Efficient query building
- Optimized view rendering
### Feature Parity
- Maintained all Django filtering capabilities
- Preserved UI/UX patterns
- Kept identical data presentation
- Matched search algorithm functionality
## Technical Details
### Query Building
```php
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}%");
});
})
// Additional filter conditions...
->paginate(10);
}
```
### Filter State Management
```php
protected $queryString = [
'search' => ['except' => ''],
'location' => ['except' => ''],
'minRating' => ['except' => ''],
'maxRating' => ['except' => ''],
'minRides' => ['except' => ''],
'minCoasters' => ['except' => '']
];
```
## Testing Considerations
1. Filter Combinations
- Test various filter combinations
- Verify result accuracy
- Check edge cases
2. Performance Testing
- Large result sets
- Multiple concurrent users
- Query optimization
3. UI Testing
- Mobile responsiveness
- Dark mode functionality
- Accessibility compliance
## Future Enhancements
1. Advanced Filters
- Date range filtering
- Category filtering
- Geographic radius search
2. Performance Optimizations
- Result caching
- Lazy loading options
- Query optimization
3. UI Improvements
- Save search preferences
- Filter presets
- Advanced sorting options

View File

@@ -0,0 +1,145 @@
# Review Components Implementation
## Task Overview
Implement the Livewire components for the ride review system, following the Laravel/Livewire implementation guidelines and maintaining feature parity with the Django implementation.
## Components to Implement
### 1. RideReviewComponent
Create a new Livewire component for submitting ride reviews:
```php
php artisan make:livewire RideReviewComponent
```
Requirements:
- Form for submitting new reviews
- Rating input (1-5 stars)
- Optional title field
- Required content field
- Real-time validation
- Success/error messaging
- Anti-spam measures
- Check if user can review (one per ride)
### 2. RideReviewListComponent
Create a component to display ride reviews:
```php
php artisan make:livewire RideReviewListComponent
```
Requirements:
- Grid/list view of reviews
- Pagination support
- Sorting options (date, rating)
- Filter by rating
- Helpful vote functionality
- Display review statistics
- Responsive design matching Django
### 3. ReviewModerationComponent
Create a moderation interface component:
```php
php artisan make:livewire ReviewModerationComponent
```
Requirements:
- Review queue display
- Approve/reject actions
- Edit capabilities
- Status tracking
- Moderation history
- Permission checks
## Implementation Guidelines
1. Use Livewire's Real-time Validation
- Validate rating range (1-5)
- Required fields
- Content length limits
- Anti-spam rules
2. State Management
- Track form state
- Handle validation errors
- Manage success/error messages
- Maintain sort/filter state
3. Event Handling
- Review submission events
- Helpful vote toggling
- Moderation actions
- Pagination events
4. View Templates
- Create Blade views for each component
- Follow project's design system
- Maintain responsive design
- Support dark mode
5. Authorization
- Use Laravel's authorization system
- Check review permissions
- Verify moderation access
- Rate limiting implementation
## Testing Requirements
1. Feature Tests
- Review submission
- Validation rules
- Helpful votes
- Moderation flow
2. Component Tests
- Real-time validation
- State management
- Event handling
- Authorization
3. View Tests
- Rendering logic
- Responsive design
- Dark mode support
## Documentation Updates
1. Update Memory Bank
- Document component implementations
- Track progress
- Note any deviations
- Update technical decisions
2. Update Component Docs
- Usage examples
- Props/events
- State management
- Authorization rules
## Next Steps
1. Create RideReviewComponent
- Implement form layout
- Add validation rules
- Handle submission
- Add success/error states
2. Build RideReviewListComponent
- Create list/grid views
- Add sorting/filtering
- Implement pagination
- Add helpful votes
3. Develop ReviewModerationComponent
- Build moderation queue
- Add approval workflow
- Implement edit features
- Track moderation history
4. Integration
- Add to ride detail page
- Connect moderation panel
- Test all interactions
- Verify feature parity

View File

@@ -0,0 +1,280 @@
<div class="space-y-6">
{{-- Message --}}
@if ($message)
<div @class([
'p-4 mb-4 rounded-lg',
'bg-green-100 dark:bg-green-800 text-green-700 dark:text-green-200' => !str_contains($message, 'error'),
'bg-red-100 dark:bg-red-800 text-red-700 dark:text-red-200' => str_contains($message, 'error'),
])>
{{ $message }}
</div>
@endif
{{-- Controls --}}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow">
<div class="p-4 space-y-4">
{{-- Status Tabs --}}
<div class="border-b border-gray-200 dark:border-gray-700">
<nav class="-mb-px flex space-x-8">
<button
wire:click="filterByStatus('{{ ReviewStatus::PENDING->value }}')"
@class([
'pb-4 px-1 border-b-2 font-medium text-sm',
'border-primary-500 text-primary-600' => $statusFilter === ReviewStatus::PENDING->value,
'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' => $statusFilter !== ReviewStatus::PENDING->value,
])
>
Pending
@if ($totalPending > 0)
<span class="ml-2 bg-primary-100 text-primary-600 py-0.5 px-2 rounded-full text-xs">
{{ $totalPending }}
</span>
@endif
</button>
<button
wire:click="filterByStatus('{{ ReviewStatus::APPROVED->value }}')"
@class([
'pb-4 px-1 border-b-2 font-medium text-sm',
'border-primary-500 text-primary-600' => $statusFilter === ReviewStatus::APPROVED->value,
'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' => $statusFilter !== ReviewStatus::APPROVED->value,
])
>
Approved
</button>
<button
wire:click="filterByStatus('{{ ReviewStatus::REJECTED->value }}')"
@class([
'pb-4 px-1 border-b-2 font-medium text-sm',
'border-primary-500 text-primary-600' => $statusFilter === ReviewStatus::REJECTED->value,
'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' => $statusFilter !== ReviewStatus::REJECTED->value,
])
>
Rejected
</button>
</nav>
</div>
{{-- Search & Batch Actions --}}
<div class="flex items-center justify-between">
<div class="max-w-lg flex-1">
<label for="search" class="sr-only">Search reviews</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-gray-500 dark:text-gray-400">
🔍
</span>
</div>
<input
type="search"
wire:model.live.debounce.300ms="search"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md leading-5 bg-white dark:bg-gray-700 placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="Search reviews..."
>
</div>
</div>
@if (count($selected) > 0)
<div class="flex items-center space-x-3">
<span class="text-sm text-gray-700 dark:text-gray-300">
{{ count($selected) }} selected
</span>
<button
wire:click="batchApprove"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
Approve Selected
</button>
<button
wire:click="batchReject"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Reject Selected
</button>
</div>
@endif
</div>
</div>
</div>
{{-- Reviews List --}}
<div class="bg-white dark:bg-gray-800 shadow rounded-lg">
<ul role="list" class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse ($reviews as $review)
<li class="p-4">
<div class="flex items-start space-x-4">
{{-- Checkbox --}}
<div class="flex-shrink-0">
<input
type="checkbox"
value="{{ $review->id }}"
wire:model.live="selected"
class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
>
</div>
{{-- Content --}}
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $review->user->name }}
</div>
<div class="text-sm text-gray-500">
{{ $review->created_at->diffForHumans() }}
</div>
</div>
<div class="mt-1">
<div class="flex items-center space-x-2">
<div class="text-yellow-400">
@for ($i = 1; $i <= 5; $i++)
<span @class(['opacity-40' => $i > $review->rating])></span>
@endfor
</div>
@if ($review->title)
<span class="text-gray-900 dark:text-gray-100 font-medium">
{{ $review->title }}
</span>
@endif
</div>
<p class="mt-1 text-gray-600 dark:text-gray-400">
{{ $review->content }}
</p>
<div class="mt-2 text-sm text-gray-500">
For: {{ $review->ride->name }}
</div>
</div>
</div>
{{-- Actions --}}
<div class="flex-shrink-0 flex items-center space-x-2">
<button
wire:click="editReview({{ $review->id }})"
class="inline-flex items-center p-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
✏️
</button>
<button
wire:click="approve({{ $review->id }})"
class="inline-flex items-center p-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
</button>
<button
wire:click="reject({{ $review->id }})"
class="inline-flex items-center p-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
</button>
</div>
</div>
</li>
@empty
<li class="p-4 text-center text-gray-500 dark:text-gray-400">
No reviews found.
</li>
@endforelse
</ul>
{{-- Pagination --}}
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
{{ $reviews->links() }}
</div>
</div>
{{-- Edit Modal --}}
@if ($showEditModal)
<div
class="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
>
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
{{-- Background overlay --}}
<div
class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"
></div>
{{-- Modal panel --}}
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<form wire:submit="saveEdit">
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="space-y-4">
{{-- Rating --}}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Rating
</label>
<div class="mt-1 flex items-center space-x-2">
@for ($i = 1; $i <= 5; $i++)
<button
type="button"
wire:click="$set('form.rating', {{ $i }})"
class="text-2xl focus:outline-none"
>
<span @class([
'text-yellow-400' => $i <= $form['rating'],
'text-gray-300 dark:text-gray-600' => $i > $form['rating'],
])></span>
</button>
@endfor
</div>
@error('form.rating')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- Title --}}
<div>
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Title
</label>
<input
type="text"
wire:model="form.title"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm dark:bg-gray-700"
>
@error('form.title')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- Content --}}
<div>
<label for="content" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Content
</label>
<textarea
wire:model="form.content"
rows="4"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm dark:bg-gray-700"
></textarea>
@error('form.content')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
</div>
</div>
{{-- Modal footer --}}
<div class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="submit"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
>
Save Changes
</button>
<button
type="button"
wire:click="$set('showEditModal', false)"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-base font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
@endif
</div>

View File

@@ -0,0 +1,101 @@
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
{{-- Show message if exists --}}
@if ($message)
<div @class([
'p-4 mb-4 rounded-lg',
'bg-green-100 dark:bg-green-800 text-green-700 dark:text-green-200' => !str_contains($message, 'error') && !str_contains($message, 'cannot'),
'bg-red-100 dark:bg-red-800 text-red-700 dark:text-red-200' => str_contains($message, 'error') || str_contains($message, 'cannot'),
])>
{{ $message }}
</div>
@endif
{{-- Review Form --}}
<form wire:submit="save" class="space-y-6">
{{-- Star Rating --}}
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Rating <span class="text-red-500">*</span>
</label>
<div class="flex items-center space-x-2">
@for ($i = 1; $i <= 5; $i++)
<button
type="button"
wire:click="$set('rating', {{ $i }})"
class="text-2xl focus:outline-none"
>
<span @class([
'text-yellow-400' => $i <= $rating,
'text-gray-300 dark:text-gray-600' => $i > $rating,
])></span>
</button>
@endfor
</div>
@error('rating')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- Title Field --}}
<div>
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Title <span class="text-gray-500">(optional)</span>
</label>
<input
type="text"
id="title"
wire:model="title"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 shadow-sm focus:border-primary-500 focus:ring-primary-500"
placeholder="Give your review a title"
>
@error('title')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- Content Field --}}
<div>
<label for="content" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Review <span class="text-red-500">*</span>
</label>
<textarea
id="content"
wire:model="content"
rows="4"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 shadow-sm focus:border-primary-500 focus:ring-primary-500"
placeholder="Share your experience with this ride"
></textarea>
<p class="mt-1 text-sm text-gray-500">
{{ strlen($content) }}/2000 characters
</p>
@error('content')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- Submit Button --}}
<div class="flex justify-end space-x-3">
@if ($isEditing)
<button
type="button"
wire:click="resetForm"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Reset
</button>
@endif
<button
type="submit"
wire:loading.attr="disabled"
class="inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-primary-600 border border-transparent rounded-md shadow-sm hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
>
<span wire:loading.remove>
{{ $isEditing ? 'Update Review' : 'Submit Review' }}
</span>
<span wire:loading>
{{ $isEditing ? 'Updating...' : 'Submitting...' }}
</span>
</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,189 @@
<div class="space-y-6">
{{-- Message --}}
@if ($message)
<div @class([
'p-4 mb-4 rounded-lg',
'bg-green-100 dark:bg-green-800 text-green-700 dark:text-green-200' => !str_contains($message, 'error') && !str_contains($message, 'cannot'),
'bg-red-100 dark:bg-red-800 text-red-700 dark:text-red-200' => str_contains($message, 'error') || str_contains($message, 'cannot'),
])>
{{ $message }}
</div>
@endif
{{-- Statistics Panel --}}
<div x-data="{ open: @entangle('showStats') }" class="bg-white dark:bg-gray-800 rounded-lg shadow">
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
<button
type="button"
@click="open = !open"
class="flex justify-between items-center w-full"
>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
Review Statistics
</h3>
<span class="transform" :class="{ 'rotate-180': open }">
</span>
</button>
</div>
<div x-show="open" class="p-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="text-center">
<div class="text-3xl font-bold text-gray-900 dark:text-gray-100">
{{ $statistics['average'] }}
</div>
<div class="text-sm text-gray-500">
Average Rating
</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-gray-900 dark:text-gray-100">
{{ $statistics['total'] }}
</div>
<div class="text-sm text-gray-500">
Total Reviews
</div>
</div>
<div class="col-span-1 md:col-span-1">
@foreach (range(5, 1) as $rating)
<div class="flex items-center">
<span class="w-8 text-sm text-gray-600 dark:text-gray-400">
{{ $rating }}
</span>
<div
x-data="{ width: '{{ $statistics['total'] > 0 ? number_format(($statistics['distribution'][$rating] / $statistics['total']) * 100, 1) : 0 }}%' }"
class="flex-1 h-4 mx-2 bg-gray-200 dark:bg-gray-700 rounded relative overflow-hidden"
>
@if ($statistics['total'] > 0)
<div
class="h-4 bg-yellow-400 rounded absolute inset-y-0 left-0"
:style="{ width }"
></div>
@endif
</div>
<span class="w-8 text-sm text-gray-600 dark:text-gray-400">
{{ $statistics['distribution'][$rating] }}
</span>
</div>
@endforeach
</div>
</div>
</div>
</div>
{{-- Controls --}}
<div class="flex flex-wrap gap-4 items-center justify-between">
{{-- Sort Controls --}}
<div class="flex gap-2">
<button
wire:click="sortBy('created_at')"
@class([
'px-3 py-2 text-sm font-medium rounded-md',
'bg-primary-100 text-primary-700 dark:bg-primary-800 dark:text-primary-200' => $sortField === 'created_at',
'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700' => $sortField !== 'created_at',
])
>
Date
@if ($sortField === 'created_at')
<span>{{ $sortDirection === 'asc' ? '↑' : '↓' }}</span>
@endif
</button>
<button
wire:click="sortBy('rating')"
@class([
'px-3 py-2 text-sm font-medium rounded-md',
'bg-primary-100 text-primary-700 dark:bg-primary-800 dark:text-primary-200' => $sortField === 'rating',
'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700' => $sortField !== 'rating',
])
>
Rating
@if ($sortField === 'rating')
<span>{{ $sortDirection === 'asc' ? '↑' : '↓' }}</span>
@endif
</button>
</div>
{{-- Rating Filter --}}
<div class="flex gap-2">
@foreach (range(1, 5) as $rating)
<button
wire:click="filterByRating({{ $rating }})"
@class([
'px-3 py-2 text-sm font-medium rounded-md',
'bg-yellow-100 text-yellow-700 dark:bg-yellow-800 dark:text-yellow-200' => $ratingFilter === $rating,
'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700' => $ratingFilter !== $rating,
])
>
{{ $rating }}
</button>
@endforeach
@if ($ratingFilter)
<button
wire:click="filterByRating(null)"
class="px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 rounded-md"
>
Clear
</button>
@endif
</div>
</div>
{{-- Reviews List --}}
<div class="space-y-4">
@forelse ($reviews as $review)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex justify-between items-start">
<div>
<div class="flex items-center gap-2">
<div class="text-yellow-400 text-xl">
@for ($i = 1; $i <= 5; $i++)
<span @class(['opacity-40' => $i > $review->rating])></span>
@endfor
</div>
@if ($review->title)
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ $review->title }}
</h3>
@endif
</div>
<p class="mt-2 text-gray-600 dark:text-gray-400">
{{ $review->content }}
</p>
</div>
<div class="text-sm text-gray-500">
{{ $review->created_at->diffForHumans() }}
</div>
</div>
<div class="mt-4 flex items-center justify-between">
<div class="text-sm text-gray-500">
By {{ $review->user->name }}
</div>
<button
wire:click="toggleHelpfulVote({{ $review->id }})"
@class([
'flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md',
'bg-green-100 text-green-700 dark:bg-green-800 dark:text-green-200' => $review->helpfulVotes->contains('user_id', Auth::id()),
'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700' => !$review->helpfulVotes->contains('user_id', Auth::id()),
])
>
<span>Helpful</span>
<span>({{ $review->helpfulVotes->count() }})</span>
</button>
</div>
</div>
@empty
<div class="text-center py-12 text-gray-500 dark:text-gray-400">
@if ($ratingFilter)
No {{ $ratingFilter }}-star reviews yet.
@else
No reviews yet.
@endif
</div>
@endforelse
{{-- Pagination --}}
<div class="mt-6">
{{ $reviews->links() }}
</div>
</div>
</div>

View File

@@ -0,0 +1,189 @@
<div class="container mx-auto px-4 py-8">
<div class="flex flex-col lg:flex-row gap-8">
<!-- Filters Sidebar -->
<div class="lg:w-1/4">
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4 dark:text-white">Filter Parks</h2>
<div class="space-y-4">
<!-- Search -->
<div class="flex flex-col">
<label for="search" class="text-sm font-medium text-gray-700 dark:text-gray-300">
Search
</label>
<div class="mt-1">
<input type="text"
wire:model.live="search"
id="search"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="Search parks...">
</div>
</div>
<!-- Location -->
<div class="flex flex-col">
<label for="location" class="text-sm font-medium text-gray-700 dark:text-gray-300">
Location
</label>
<div class="mt-1">
<input type="text"
wire:model.live="location"
id="location"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="Filter by location...">
</div>
</div>
<!-- Rating Range -->
<div class="flex flex-col">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Rating Range</label>
<div class="flex gap-2 mt-1">
<input type="number"
wire:model.live="minRating"
min="0"
max="5"
step="0.1"
class="w-1/2 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="Min">
<input type="number"
wire:model.live="maxRating"
min="0"
max="5"
step="0.1"
class="w-1/2 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="Max">
</div>
</div>
<!-- Minimum Rides -->
<div class="flex flex-col">
<label for="minRides" class="text-sm font-medium text-gray-700 dark:text-gray-300">
Minimum Rides
</label>
<div class="mt-1">
<input type="number"
wire:model.live="minRides"
id="minRides"
min="0"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="Minimum number of rides">
</div>
</div>
<!-- Minimum Coasters -->
<div class="flex flex-col">
<label for="minCoasters" class="text-sm font-medium text-gray-700 dark:text-gray-300">
Minimum Coasters
</label>
<div class="mt-1">
<input type="number"
wire:model.live="minCoasters"
id="minCoasters"
min="0"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="Minimum number of coasters">
</div>
</div>
<!-- Filter Actions -->
<div class="flex justify-between pt-2">
@if($filtersApplied)
<button wire:click="clearFilters"
type="button"
class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:bg-gray-600">
Clear Filters
</button>
@endif
</div>
</div>
</div>
</div>
<!-- Results Section -->
<div class="lg:w-3/4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<h2 class="text-xl font-bold dark:text-white">
Search Results
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">({{ $results->total() }} found)</span>
</h2>
</div>
</div>
<div class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse($results as $park)
<div class="p-6 flex flex-col md:flex-row gap-4">
<!-- Park Image -->
<div class="md:w-48 h-32 bg-gray-200 dark:bg-gray-700 rounded-lg overflow-hidden">
@if($park->photos->isNotEmpty())
<img src="{{ $park->photos->first()->image_url }}"
alt="{{ $park->name }}"
class="w-full h-full object-cover">
@else
<div class="w-full h-full flex items-center justify-center text-gray-400 dark:text-gray-500">
No Image
</div>
@endif
</div>
<!-- Park Details -->
<div class="flex-1">
<h3 class="text-lg font-semibold">
<a href="{{ route('parks.show', $park) }}" class="hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
{{ $park->name }}
</a>
</h3>
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
@if($park->formatted_location)
<p>{{ $park->formatted_location }}</p>
@endif
</div>
<div class="mt-2 flex flex-wrap gap-2">
@if($park->average_rating)
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100">
{{ number_format($park->average_rating, 1) }}
</span>
@endif
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100">
{{ $park->status->label() }}
</span>
@if($park->ride_count)
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100">
{{ $park->ride_count }} Rides
</span>
@endif
@if($park->coaster_count)
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100">
{{ $park->coaster_count }} Coasters
</span>
@endif
</div>
@if($park->description)
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{{ $park->description }}
</p>
@endif
</div>
</div>
@empty
<div class="p-6 text-center text-gray-500 dark:text-gray-400">
No parks found matching your criteria.
</div>
@endforelse
</div>
@if($results->hasPages())
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
{{ $results->links() }}
</div>
@endif
</div>
</div>
</div>
</div>

View File

@@ -79,6 +79,4 @@ Route::get('/profile/{username}', function () {
})->name('profile.show');
// Search route
Route::get('/search', function () {
return 'Search';
})->name('search');
Route::get('/search', \App\Livewire\SearchComponent::class)->name('search');