From 0f86c78248ec89ae1f403c02d323791f636526c5 Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 6 Jan 2026 14:38:51 +0100 Subject: [PATCH] feat: seeder --- app/Http/Controllers/HuntStepsController.php | 95 ++++++++++++++++++++ app/Models/HuntSteps.php | 2 + database/factories/HuntStepsFactory.php | 32 +++++++ database/factories/HuntsFactory.php | 36 ++++++++ database/seeders/BordeauxHuntsSeeder.php | 71 +++++++++++++++ database/seeders/DatabaseSeeder.php | 6 +- routes/api.php | 1 + 7 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 database/factories/HuntStepsFactory.php create mode 100644 database/factories/HuntsFactory.php create mode 100644 database/seeders/BordeauxHuntsSeeder.php diff --git a/app/Http/Controllers/HuntStepsController.php b/app/Http/Controllers/HuntStepsController.php index 5b3bb11..b69d8da 100644 --- a/app/Http/Controllers/HuntStepsController.php +++ b/app/Http/Controllers/HuntStepsController.php @@ -38,4 +38,99 @@ class HuntStepsController extends Controller return response()->json($step, 201); } + + /** + * Validate a hunt step. + */ + public function validateStep(\Illuminate\Http\Request $request, \App\Models\Hunts $hunt, \App\Models\HuntSteps $step) + { + $request->validate([ + 'latitude' => 'required|numeric', + 'longitude' => 'required|numeric', + ]); + + $user = $request->user(); + + // 1. Check participation + $participation = $user->participations()->where('hunt_id', $hunt->id)->first(); + + if (!$participation) { + return response()->json(['message' => 'Not participating in this hunt'], 403); + } + + // 2. Check if this is the current step + if ($participation->pivot->current_step_number !== $step->step_number) { + return response()->json(['message' => 'This is not your current step'], 400); + } + + // 3. Check proximity + // Haversine formula + $earthRadius = 6371000; // meters + + $latFrom = deg2rad($request->latitude); + $lonFrom = deg2rad($request->longitude); + $latTo = deg2rad($step->latitude); + $lonTo = deg2rad($step->longitude); + + $latDelta = $latTo - $latFrom; + $lonDelta = $lonTo - $lonFrom; + + $angle = 2 * asin(sqrt(pow(sin($latDelta / 2), 2) + + cos($latFrom) * cos($latTo) * pow(sin($lonDelta / 2), 2))); + + $distance = $angle * $earthRadius; + + if ($distance > $step->radius_m) { + return response()->json([ + 'success' => false, + 'message' => 'You are too far from the target.', + 'distance' => round($distance) . 'm' + ], 400); + } + + // 4. Update progress + // Check if there is a next step + $nextStepNumber = $step->step_number + 1; + $nextStep = $hunt->steps()->where('step_number', $nextStepNumber)->first(); + + $updateData = []; + + if ($nextStep) { + $updateData['current_step_number'] = $nextStepNumber; + } else { + $updateData['status'] = 'completed'; + // Keep current_step_number as is (last step) or increment it to indicate "done"? + // Usually keeping it at last step or marking status completed is enough. + // Let's verify existing logic in HuntsController::show which filters based on step number. + // If we increment, show() might need adjustment to show everything if completed. + // For now, let's just mark completed and maybe keep same step number or increment. + // If we increment, $nextStep is null, so user matches nothing in filter? + // Let's increment current_step_number so that in show(), $step->step_number < $currentStepNumber becomes true for all steps. + $updateData['current_step_number'] = $nextStepNumber; + } + + $hunt->participants()->updateExistingPivot($user->id, $updateData); + + // 5. Return response + $response = [ + 'success' => true, + 'message' => 'Step validated!', + ]; + + if ($nextStep) { + $response['next_step'] = [ + 'step_number' => $nextStep->step_number, + 'title' => $nextStep->title, + 'description' => $nextStep->description, + 'hint_text' => $nextStep->hint_text, + 'hint_media_url' => $nextStep->hint_media_url, + // Do NOT include lat/long + ]; + } else { + $response['message'] = 'Hunt completed! Congratulations!'; + $response['completed'] = true; + } + + return response()->json($response); + } } diff --git a/app/Models/HuntSteps.php b/app/Models/HuntSteps.php index 488bdd7..9857256 100644 --- a/app/Models/HuntSteps.php +++ b/app/Models/HuntSteps.php @@ -2,10 +2,12 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class HuntSteps extends Model { + use HasFactory; protected $fillable = [ 'hunt_id', 'step_number', diff --git a/database/factories/HuntStepsFactory.php b/database/factories/HuntStepsFactory.php new file mode 100644 index 0000000..fca0e99 --- /dev/null +++ b/database/factories/HuntStepsFactory.php @@ -0,0 +1,32 @@ + + */ +class HuntStepsFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'hunt_id' => \App\Models\Hunts::factory(), + 'step_number' => $this->faker->numberBetween(1, 10), + 'title' => $this->faker->sentence(2), + 'description' => $this->faker->paragraph(), + 'hint_text' => $this->faker->sentence(), + 'hint_media_url' => $this->faker->imageUrl(), + 'latitude' => $this->faker->latitude(), + 'longitude' => $this->faker->longitude(), + 'radius_m' => 20, + 'xp' => 100, + ]; + } +} diff --git a/database/factories/HuntsFactory.php b/database/factories/HuntsFactory.php new file mode 100644 index 0000000..7f70a6b --- /dev/null +++ b/database/factories/HuntsFactory.php @@ -0,0 +1,36 @@ + + */ +class HuntsFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'creator_id' => \App\Models\User::factory(), + 'title' => $this->faker->sentence(3), + 'slug' => $this->faker->slug(), + 'description' => $this->faker->paragraph(), + 'status' => \App\Enums\HuntStatus::PUBLISHED, + 'difficulty' => \App\Enums\HuntDifficulty::EASY, + 'city' => $this->faker->city(), + 'latitude' => $this->faker->latitude(), + 'longitude' => $this->faker->longitude(), + 'is_public' => true, + 'estimated_duration_min' => $this->faker->numberBetween(30, 180), + 'image' => $this->faker->imageUrl(), + 'start_at' => now(), + 'end_at' => null, + ]; + } +} diff --git a/database/seeders/BordeauxHuntsSeeder.php b/database/seeders/BordeauxHuntsSeeder.php new file mode 100644 index 0000000..5e74700 --- /dev/null +++ b/database/seeders/BordeauxHuntsSeeder.php @@ -0,0 +1,71 @@ +create([ + 'username' => 'Bordeaux Organizer', + 'email' => 'orga@bordeaux.fr', + 'password' => bcrypt('password'), + 'role' => \App\Enums\UserRole::ORGA, + ]); + + // 2. Create Hunts in Bordeaux + $hunts = \App\Models\Hunts::factory()->count(3)->create([ + 'creator_id' => $creator->id, + 'city' => 'Bordeaux', + 'latitude' => 44.837789, + 'longitude' => -0.57918, + 'status' => \App\Enums\HuntStatus::PUBLISHED, + ]); + + // 3. Create Steps for each Hunt + foreach ($hunts as $hunt) { + // Step 1: Place de la Bourse + \App\Models\HuntSteps::factory()->create([ + 'hunt_id' => $hunt->id, + 'step_number' => 1, + 'title' => 'Place de la Bourse', + 'description' => 'Rendez-vous sur cette place emblématique.', + 'hint_text' => 'Miroir d\'eau', + 'latitude' => 44.8417, + 'longitude' => -0.5706, + 'radius_m' => 50, + ]); + + // Step 2: Grand Théâtre + \App\Models\HuntSteps::factory()->create([ + 'hunt_id' => $hunt->id, + 'step_number' => 2, + 'title' => 'Grand Théâtre', + 'description' => 'Un opéra du XVIIIe siècle.', + 'hint_text' => 'Douze colonnes corinthiennes', + 'latitude' => 44.8427, + 'longitude' => -0.5746, + 'radius_m' => 50, + ]); + + // Step 3: Cathédrale Saint-André + \App\Models\HuntSteps::factory()->create([ + 'hunt_id' => $hunt->id, + 'step_number' => 3, + 'title' => 'Cathédrale Saint-André', + 'description' => 'Le plus beau monument religieux de Bordeaux.', + 'hint_text' => 'Tour Pey Berland', + 'latitude' => 44.8378, + 'longitude' => -0.5775, + 'radius_m' => 50, + ]); + } + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..0ad7c61 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -15,11 +15,9 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + $this->call([ + BordeauxHuntsSeeder::class, ]); } } diff --git a/routes/api.php b/routes/api.php index fb808d7..b952522 100644 --- a/routes/api.php +++ b/routes/api.php @@ -39,6 +39,7 @@ Route::middleware('auth:sanctum')->group(function (): void { Route::post('users/{user}/avatar', [UsersController::class, 'updateAvatar']); Route::post('hunts/{hunt}/participate', [HuntsController::class, 'participate']); Route::delete('hunts/{hunt}/participate', [HuntsController::class, 'unparticipate']); + Route::post('hunts/{hunt}/steps/{step}/validate', [HuntStepsController::class, 'validateStep']); Route::get('auth/me', [AuthController::class, 'me']); Route::post('auth/logout', [AuthController::class, 'logout']); }); \ No newline at end of file