Skip to content

HenkHR/NatuurMoment

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

259 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌿 NatuurMoment

An interactive group game that guides players through nature areas using their phones. Players complete bingo challenges by taking photos and answer multiple-choice questions about the location, competing for the highest score.

PHP Laravel Livewire Tailwind CSS


πŸ“‹ Table of Contents

  1. Features
  2. Technology Stack
  3. Entity Relationship Diagram (ERD)
  4. Installation
  5. Deployment
  6. Configuration
  7. Edge Cases & Special Handling
  8. Project Structure
  9. Testing

✨ Features
  • Game Hosting: Create games with unique PIN codes for players to join
  • Bingo Mode: Players capture photos of nature items to complete a 3x3 bingo card
  • Question Mode: Sequential multiple-choice questions about the location
  • Real-time Leaderboards: Track player scores and progress in real-time
  • Photo Management: Host approves/rejects player photos with feedback
  • Admin Panel: Full CRUD interface for managing locations, bingo items, and questions
  • Game Modes: Configurable game modes per location (Bingo, Questions, or both)
  • Timer Support: Optional countdown timer for games
  • Player Feedback: Post-game feedback collection (rating and age)

πŸ›  Technology Stack
Component Technology Version
Backend Framework Laravel ^12.0
Frontend Framework Livewire ^3.7
PHP Version PHP ^8.2
CSS Framework Tailwind CSS ^3.4
JavaScript Framework Alpine.js ^3.4
Charts Chart.js ^4.4.1
Icons Blade Icons (Heroicons, Lucide, Solar, Bootstrap) Various
Fonts Lexend, Figtree Google/Bunny Fonts
Build Tool Vite ^7.0
Storage AWS S3 / Cloudflare R2 Optional
Testing Framework Pest PHP ^4.1

πŸ—„ Entity Relationship Diagram (ERD)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                           TEMPLATE LAYER                                                     β”‚
β”‚                                    (Admin-managed location templates)                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           locations             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ PK  id                          β”‚
β”‚     name                        β”‚
β”‚     description                 β”‚
β”‚     image_path                  β”‚
β”‚     province                    β”‚
β”‚     distance                    β”‚
β”‚     url                         β”‚
β”‚     game_modes (JSON)           β”‚
β”‚     bingo_three_in_row_points   β”‚
β”‚     bingo_full_card_points      β”‚
β”‚     created_at                  β”‚
β”‚     updated_at                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β”‚ 1:N
           β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
           β”‚                                          β”‚
           β–Ό                                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    location_bingo_items         β”‚    β”‚    location_route_stops         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ PK  id                          β”‚    β”‚ PK  id                          β”‚
β”‚ FK  location_id                 β”‚    β”‚ FK  location_id                 β”‚
β”‚     label                       β”‚    β”‚     name                        β”‚
β”‚     points                      β”‚    β”‚     question_text               β”‚
β”‚     icon                        β”‚    β”‚     option_a/b/c/d              β”‚
β”‚     fact                        β”‚    β”‚     correct_option (ENUM)       β”‚
β”‚     created_at                  β”‚    β”‚     points                      β”‚
β”‚     updated_at                  β”‚    β”‚     sequence                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚     image_path                  β”‚
                                       β”‚     created_at                  β”‚
                                       β”‚     updated_at                  β”‚
                                       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                           INSTANCE LAYER                                                     β”‚
β”‚                                     (Runtime game instances & data)                                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

                                       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                       β”‚            games                β”‚
                                       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                                       β”‚ PK  id                          β”‚
              locations.id ◄───────────│ FK  location_id                 β”‚
                                       β”‚     pin (UNIQUE)                β”‚
                                       β”‚     status (ENUM)               β”‚
                                       β”‚     host_token                  β”‚
                                       β”‚     timer_enabled               β”‚
                                       β”‚     timer_duration_minutes      β”‚
                                       β”‚     timer_ends_at               β”‚
                                       β”‚     started_at / finished_at    β”‚
                                       β”‚     created_at / updated_at     β”‚
                                       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                      β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚ 1:N                             β”‚ 1:N                             β”‚ 1:N
                    β–Ό                                 β–Ό                                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         game_players            β”‚  β”‚         bingo_items             β”‚  β”‚         route_stops             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ PK  id                          β”‚  β”‚ PK  id                          β”‚  β”‚ PK  id                          β”‚
β”‚ FK  game_id                     β”‚  β”‚ FK  game_id                     β”‚  β”‚ FK  game_id                     β”‚
β”‚     name                        β”‚  β”‚     label                       β”‚  β”‚     name                        β”‚
β”‚     token (UNIQUE)              β”‚  β”‚     points                      β”‚  β”‚     question_text               β”‚
β”‚     score                       β”‚  β”‚     position                    β”‚  β”‚     option_a/b/c/d              β”‚
β”‚     feedback_rating             β”‚  β”‚     icon_path                   β”‚  β”‚     correct_option (ENUM)       β”‚
β”‚     feedback_age                β”‚  β”‚     created_at                  β”‚  β”‚     points                      β”‚
β”‚     created_at                  β”‚  β”‚     updated_at                  β”‚  β”‚     sequence                    β”‚
β”‚     updated_at                  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚     image_path                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚                     β”‚     created_at                  β”‚
           β”‚                                        β”‚                     β”‚     updated_at                  β”‚
           β”‚ 1:N                                    β”‚ 1:N                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚                                        β”‚                                    β”‚
           β–Ό                                        β–Ό                                    β”‚ 1:N
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚
    β”‚      route_stop_answers         β”‚      β”‚           photos                β”‚        β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€      β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€        β”‚
    β”‚ PK  id                          β”‚      β”‚ PK  id                          β”‚        β”‚
    β”‚ FK  game_player_id              β”‚      β”‚ FK  game_id                     β”‚        β”‚
    β”‚ FK  route_stop_id ◄─────────────│──────│ FK  game_player_id              β”‚        β”‚
    β”‚     chosen_option (ENUM)        β”‚      β”‚ FK  bingo_item_id β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚     is_correct                  β”‚      β”‚     path                        β”‚
    β”‚     score_awarded               β”‚      β”‚     status (ENUM)               β”‚
    β”‚     answered_at                 β”‚      β”‚     taken_at                    β”‚
    β”‚     created_at                  β”‚      β”‚     created_at                  β”‚
    β”‚     updated_at                  β”‚      β”‚     updated_at                  β”‚
    β”‚                                 β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚ UNIQUE(game_player_id,          β”‚
    β”‚        route_stop_id)           β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                           SYSTEM LAYER (Admin users)                                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            users                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ PK  id                          β”‚
β”‚     name                        β”‚
β”‚     email (UNIQUE)              β”‚
β”‚     password                    β”‚
β”‚     is_admin                    β”‚
β”‚     admin_per_page              β”‚
β”‚     created_at                  β”‚
β”‚     updated_at                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Relationships

Parent Table Child Table Relationship Foreign Key
locations location_bingo_items 1:N location_id
locations location_route_stops 1:N location_id
locations games 1:N location_id
games game_players 1:N game_id
games bingo_items 1:N game_id
games route_stops 1:N game_id
game_players photos 1:N game_player_id
game_players route_stop_answers 1:N game_player_id
bingo_items photos 1:N bingo_item_id
route_stops route_stop_answers 1:N route_stop_id

ENUM Values

Table Column Values
games status lobby, started, finished
photos status pending, approved, rejected
location_route_stops / route_stops correct_option A, B, C, D
route_stop_answers chosen_option A, B, C, D

πŸš€ Installation

Prerequisites

  • PHP 8.2 or higher
  • Composer
  • Node.js 18+ and npm
  • SQLite (for development) or MySQL/PostgreSQL (for production)

Step 1: Clone the Repository

git clone <repository-url>
cd NatuurMoment

Step 2: Install Dependencies

composer install
npm install

Step 3: Environment Configuration

cp .env.example .env
php artisan key:generate

Step 4: Database Setup

php artisan migrate
php artisan db:seed  # Optional: seed with sample data

Step 5: Build Assets

npm run build       # Production build
# OR
npm run dev         # Development with hot reload

Step 6: Start the Development Server

php artisan serve
# OR use the dev script (includes queue worker and Vite)
composer run dev

The application should now be running at http://localhost:8000

Step 7: Create Admin User (Optional)

php artisan db:seed --class=DatabaseSeeder
# Default admin: admin@example.com / password

⚠️ Security: Change the default admin password immediately after first login!


🌐 Deployment

Laravel Cloud (Recommended)

This project was developed and deployed using Laravel Cloud. Laravel Cloud provides a seamless deployment experience for Laravel applications.

Deploying to Laravel Cloud

  1. Create a Laravel Cloud account at cloud.laravel.com

  2. Connect your repository - Link your GitHub repository to Laravel Cloud

  3. Create a new application - Select your repository and branch

  4. Configure environment variables - Add the following in the Laravel Cloud dashboard:

    • APP_KEY (generate with php artisan key:generate --show)
    • DB_CONNECTION, DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD
    • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_BUCKET (for photo storage)
  5. Configure build settings:

    • Build command: npm install && npm run build
    • Laravel Cloud automatically runs composer install and php artisan migrate
  6. Deploy - Push to your branch or trigger a manual deployment

Laravel Cloud handles SSL certificates, queue workers, and automatic deployments on push.


Manual Deployment

Production Requirements

  • PHP 8.2+ with extensions (openssl, pdo, mbstring, tokenizer, xml, ctype, json, fileinfo)
  • MySQL 5.7+ or PostgreSQL 10+
  • Web server (Nginx or Apache) with mod_rewrite
  • SSL certificate (HTTPS required for photo uploads)

Deployment Steps

# Install dependencies
composer install --optimize-autoloader --no-dev
npm install && npm run build

# Configure environment
cp .env.example .env
php artisan key:generate

# Run migrations
php artisan migrate --force

# Optimize for production
php artisan config:cache
php artisan route:cache
php artisan view:cache

βš™οΈ Configuration

Key Environment Variables

Variable Description Example
APP_ENV Environment production
APP_DEBUG Debug mode (false in production!) false
APP_URL Application URL https://natuurmoment.example.com
DB_CONNECTION Database driver mysql
AWS_ACCESS_KEY_ID AWS/R2 access key (optional) your_access_key
AWS_SECRET_ACCESS_KEY AWS/R2 secret key (optional) your_secret_key
AWS_BUCKET S3/R2 bucket name (optional) natuurmoment-photos

File Storage

  • Local Storage: Photos stored in storage/app/public/photos by default
  • Cloud Storage: Configure AWS credentials in .env to use S3/R2
  • The Photo model automatically falls back to local storage if cloud is unavailable

⚠️ Edge Cases & Special Handling

1. Sequential Question Unlocking

Players must answer questions in sequence. Question N+1 is only unlocked after question N is answered.

  • Solution: RouteStop::isUnlockedFor() checks if all previous questions are answered
  • Direct URL access: Prevented by validation in PlayerRouteQuestion component
  • Browser refresh: State persisted in database, players continue from current question

2. Duplicate Answer Prevention

Players cannot answer the same question twice.

  • Solution: Database unique constraint on [game_player_id, route_stop_id]
  • Race condition: Database constraint catches duplicate submissions
  • UI protection: Submit button disabled after first answer

3. Photo Approval Workflow

Host must approve photos before they count toward bingo completion.

  • Solution: Photos have status field: pending, approved, rejected
  • Completion check: Only approved photos count toward 9-photo requirement

4. Game Mode Validation

Locations must have sufficient content for enabled game modes.

  • Bingo Mode: Requires at least 9 bingo items
  • Question Mode: Requires at least 1 question
  • Validation: Locations without valid game modes hidden from home page

5. PIN Collision Prevention

Game PINs must be unique.

  • Solution: Game::generatePin() uses do-while loop to ensure uniqueness
  • Database constraint: Unique index on pin column

6. Photo Storage Fallback

Cloud storage may be unavailable.

  • Solution: Photo::getUrlAttribute() automatically falls back to local storage
  • Error handling: Exceptions during cloud storage checks are caught and logged

πŸ“ Project Structure
NatuurMoment/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ Constants/          # Game mode constants
β”‚   β”œβ”€β”€ Http/
β”‚   β”‚   β”œβ”€β”€ Controllers/    # REST controllers
β”‚   β”‚   β”œβ”€β”€ Middleware/     # Custom middleware (IsAdmin)
β”‚   β”‚   └── Requests/       # Form request validation
β”‚   β”œβ”€β”€ Livewire/           # Livewire components
β”‚   β”‚   β”œβ”€β”€ CreateGame.php
β”‚   β”‚   β”œβ”€β”€ HostGame.php
β”‚   β”‚   β”œβ”€β”€ HostLobby.php
β”‚   β”‚   β”œβ”€β”€ JoinGame.php
β”‚   β”‚   β”œβ”€β”€ PlayerPhotoCapture.php
β”‚   β”‚   β”œβ”€β”€ PlayerRouteQuestion.php
β”‚   β”‚   └── ...
β”‚   β”œβ”€β”€ Models/             # Eloquent models
β”‚   └── Rules/              # Custom validation rules
β”œβ”€β”€ config/                 # Configuration files
β”œβ”€β”€ database/
β”‚   β”œβ”€β”€ migrations/         # Database migrations
β”‚   β”œβ”€β”€ seeders/            # Database seeders
β”‚   └── factories/          # Model factories
β”œβ”€β”€ public/                 # Public assets
β”œβ”€β”€ resources/
β”‚   β”œβ”€β”€ css/                # Tailwind CSS
β”‚   β”œβ”€β”€ js/                 # JavaScript/Alpine.js
β”‚   └── views/              # Blade templates
β”‚       β”œβ”€β”€ admin/          # Admin panel views
β”‚       └── livewire/       # Livewire component views
β”œβ”€β”€ routes/                 # Route definitions
β”œβ”€β”€ storage/                # File storage
└── tests/                  # Pest PHP tests

πŸ§ͺ Testing

The project uses Pest PHP for testing.

# Run all tests
php artisan test

# Run specific test file
php artisan test tests/Feature/Admin/LocationTest.php

# Run with coverage
php artisan test --coverage

Test Coverage

  • Feature Tests: Admin panel CRUD operations, authentication, game flow
  • Unit Tests: Model relationships, helper methods, validation rules
  • Livewire Tests: Component interactions, form submissions, real-time updates

πŸ“ TODO
  • Add functionality for host to play the game with the players
  • Let hosts create accounts so they can create their own routes/locations

πŸ“„ License

This project is licensed under the MIT License.


Made with ❀️ for nature enthusiasts

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages