createUsers(); $this->createMeals($users); $this->createSocialGraph($users); $this->createProgressHistory($this->demoUser($users)); }); } /** * @return Collection */ private function createUsers(): Collection { $users = collect([ [ 'name' => 'Alex Martin', 'email' => 'test@example.com', 'locale' => 'fr', 'bio' => 'Cuisine simple, course à pied et progression régulière.', 'account_verified_at' => now(), 'height' => 178, 'target_weight' => 68, 'weight_goal' => WeightGoal::LOSE_WEIGHT, 'physical_activity_level' => PhysicalActivityLevel::MODERATELY_ACTIVE, 'pace_preference' => PacePreference::NORMAL, 'sex' => UserSex::MAN, 'date_of_birth' => '1994-04-16', 'terms_accepted_at' => now(), ], [ 'name' => 'Camille Green', 'email' => 'camille@bowli.test', 'locale' => 'fr', 'bio' => 'Recettes végétales colorées et faciles à préparer.', 'account_verified_at' => now(), ], [ 'name' => 'Noah Kitchen', 'email' => 'noah@bowli.test', 'locale' => 'en', 'bio' => 'High-protein meals without complicated preparation.', ], [ 'name' => 'Lina Fraîcheur', 'email' => 'lina@bowli.test', 'locale' => 'fr', 'bio' => 'Bols frais, produits de saison et cuisine méditerranéenne.', 'account_verified_at' => now(), ], [ 'name' => 'Sam Fit Food', 'email' => 'sam@bowli.test', 'locale' => 'en', 'bio' => 'Balanced meals for active weeks.', ], [ 'name' => 'Inès Brunch', 'email' => 'ines@bowli.test', 'locale' => 'fr', 'bio' => 'Petits-déjeuners gourmands et brunchs du dimanche.', ], [ 'name' => 'Hugo Maison', 'email' => 'hugo@bowli.test', 'locale' => 'fr', 'bio' => 'Cuisine maison généreuse avec des ingrédients simples.', ], [ 'name' => 'Maya Plant', 'email' => 'maya@bowli.test', 'locale' => 'en', 'bio' => 'Plant-based bowls, soups and snacks.', 'account_verified_at' => now(), ], [ 'name' => 'Louis Endurance', 'email' => 'louis@bowli.test', 'locale' => 'fr', 'bio' => 'Des repas adaptés aux longues sorties et à la récupération.', ], ])->map( fn (array $profile): User => $this->createUser($profile) ); $this->createUser([ 'name' => 'Nouveau profil', 'email' => 'onboarding@example.com', 'locale' => 'fr', 'terms_accepted_at' => now(), ], withNutritionOnboarding: false); return $users; } /** * @param array $profile */ private function createUser(array $profile, bool $withNutritionOnboarding = true): User { $onboardingAttributes = $withNutritionOnboarding ? [ 'height' => 175, 'target_weight' => 70, 'date_of_birth' => '1990-01-01', 'sex' => UserSex::MAN, 'physical_activity_level' => PhysicalActivityLevel::MODERATELY_ACTIVE, 'weight_goal' => WeightGoal::MAINTAIN_WEIGHT, 'pace_preference' => PacePreference::NORMAL, 'onboarding_completed_at' => now(), 'nutrition_estimate_accepted_at' => now(), ] : [ 'height' => null, 'target_weight' => null, 'date_of_birth' => null, 'sex' => null, 'physical_activity_level' => null, 'weight_goal' => null, 'pace_preference' => null, 'onboarding_completed_at' => null, 'nutrition_estimate_accepted_at' => null, ]; $user = User::query()->create([ 'locale' => 'en', 'role' => UserRole::USER, 'email_verified_at' => now(), 'password' => 'password', 'avatar_url' => null, ...$onboardingAttributes, ...$profile, ]); if ($withNutritionOnboarding) { $this->createNutritionOnboarding($user); } return $user; } private function createNutritionOnboarding(User $user): void { $user->weightEntries()->create([ 'weight_kg' => 70, 'measured_at' => now(), ]); $user->nutritionPlans()->create([ 'source' => NutritionPlanSource::ONBOARDING, 'formula_version' => 'mifflin_st_jeor_v1', 'calories' => 2200, 'proteins' => 112, 'carbs' => 286, 'fats' => 61, 'resting_metabolism' => 1650, 'maintenance_calories' => 2200, 'calorie_adjustment_percent' => 0, 'calculation_details' => [ 'activityFactor' => 1.55, 'inputWeightKg' => 70, ], 'effective_from' => now(), ]); } /** * @param Collection $users */ private function createMeals(Collection $users): void { $templates = collect($this->mealTemplates()); $reviewComments = collect([ 'Très bonne idée, je vais essayer cette semaine.', 'Simple, équilibré et vraiment appétissant.', 'Les proportions sont parfaites pour mon déjeuner.', 'Testé hier soir, toute la famille a aimé.', 'Belle assiette et excellente source d’inspiration.', 'Parfait après une séance de sport.', ]); foreach (range(0, 47) as $index) { $owner = $users->get($index % $users->count()); $template = $templates->get($index % $templates->count()); $eatenAt = now()->subHours(($index * 4) + 1); if (! $owner instanceof User || ! is_array($template)) { throw new LogicException('Unable to build the demo meal collection.'); } $meal = $owner->mealPosts()->create([ 'image_url' => $template['imageUrl'], 'caption' => $template['caption'], 'calories' => $template['calories'], 'proteins' => $template['proteins'], 'carbs' => $template['carbs'], 'fats' => $template['fats'], 'title' => $template['title'], 'eaten_at' => $eatenAt, 'visibility' => MealPostVisibility::Public, 'type' => $template['type'], 'ai_generated' => $index % 9 === 0, 'diet_type' => $template['dietType'], ]); $meal->ingredients()->createMany( collect($template['ingredients']) ->values() ->map(fn (array $ingredient, int $position): array => [ ...$ingredient, 'position' => $position + 1, ]) ->all() ); $eligibleUsers = $users ->reject(fn (User $user): bool => $user->is($owner)) ->values(); $engagedUsers = $eligibleUsers ->slice($index % $eligibleUsers->count()) ->concat($eligibleUsers->take($index % $eligibleUsers->count())) ->values(); $meal->likedByUsers()->attach( $engagedUsers->take(2 + ($index % 5))->pluck('id')->all(), [ 'created_at' => $eatenAt->addMinutes(30), 'updated_at' => $eatenAt->addMinutes(30), ] ); $engagedUsers->take(2 + ($index % 2))->each( function (User $reviewer, int $reviewerIndex) use ($index, $meal, $reviewComments): void { $meal->reviews()->create([ 'rating' => 5 - (($index + $reviewerIndex) % 2), 'comment' => $reviewComments->get( ($index + $reviewerIndex) % $reviewComments->count() ), 'user_id' => $reviewer->getKey(), ]); } ); } } /** * @param Collection $users */ private function createSocialGraph(Collection $users): void { $demoUser = $this->demoUser($users); $creators = $users ->reject(fn (User $user): bool => $user->is($demoUser)) ->values(); $pivot = [ 'status' => FollowStatus::Accepted->value, 'created_at' => now()->subDays(10), 'updated_at' => now()->subDays(10), ]; $demoUser->following()->attach($creators->pluck('id')->all(), $pivot); $creators->take(5)->each( fn (User $creator) => $creator->following()->attach($demoUser->getKey(), $pivot) ); $creators->each(function (User $creator, int $index) use ($creators, $pivot): void { $nextCreator = $creators->get(($index + 1) % $creators->count()); if ($nextCreator instanceof User) { $creator->following()->attach($nextCreator->getKey(), $pivot); } }); } private function createProgressHistory(User $demoUser): void { $demoUser->weightEntries()->delete(); foreach (range(11, 0) as $weeksAgo) { $demoUser->weightEntries()->create([ 'weight_kg' => round(74.8 - ((11 - $weeksAgo) * 0.28), 2), 'measured_at' => now()->startOfDay()->subWeeks($weeksAgo), ]); } $demoUser->nutritionPlans() ->where('is_active', true) ->update([ 'calories' => 2050, 'proteins' => 130, 'carbs' => 235, 'fats' => 65, 'maintenance_calories' => 2350, 'calorie_adjustment_percent' => -0.1277, 'calculation_details' => [ 'activityFactor' => 1.55, 'inputWeightKg' => 71.72, ], ]); $workouts = collect([ ['title' => 'Course facile', 'type' => 'running', 'duration' => 2700, 'calories' => 390, 'distance' => 7200], ['title' => 'Renforcement complet', 'type' => 'strength', 'duration' => 3300, 'calories' => 310, 'distance' => 0], ['title' => 'Sortie vélo', 'type' => 'cycling', 'duration' => 5400, 'calories' => 620, 'distance' => 32000], ['title' => 'Marche active', 'type' => 'walking', 'duration' => 3600, 'calories' => 260, 'distance' => 6100], ]); foreach (range(0, 11) as $index) { $workout = $workouts->get($index % $workouts->count()); $source = $index % 4 === 0 ? WorkoutSource::STRAVA : WorkoutSource::MANUAL; if (! is_array($workout)) { throw new LogicException('Unable to build the demo workout collection.'); } $demoUser->workouts()->create([ 'external_id' => $source === WorkoutSource::STRAVA ? "demo-strava-{$index}" : null, 'source' => $source, 'type' => $workout['type'], 'title' => $workout['title'], 'duration_seconds' => $workout['duration'], 'calories_burned' => $workout['calories'], 'distance_meters' => $workout['distance'], 'started_at' => now()->subDays($index)->setTime(18, 30), ]); } } /** * @param Collection $users */ private function demoUser(Collection $users): User { $demoUser = $users->first( fn (User $user): bool => $user->email === 'test@example.com' ); if (! $demoUser instanceof User) { throw new LogicException('The demo user could not be created.'); } return $demoUser; } /** * @return list> */ private function mealTemplates(): array { return [ [ 'title' => 'Bowl saumon et avocat', 'caption' => 'Un déjeuner complet avec du riz, des légumes croquants et une sauce citronnée.', 'imageUrl' => 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?auto=format&fit=crop&w=1200&q=85', 'calories' => 620, 'proteins' => 38, 'carbs' => 58, 'fats' => 24, 'type' => MealPostType::LUNCH, 'dietType' => DietType::PESCATARIAN, 'ingredients' => [ ['ingredient' => 'Saumon', 'quantity' => 140, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Riz complet', 'quantity' => 160, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Avocat', 'quantity' => 1, 'unit' => IngredientUnit::PIECE], ], ], [ 'title' => 'Pancakes aux fruits rouges', 'caption' => 'Des pancakes moelleux accompagnés de fruits rouges et de yaourt.', 'imageUrl' => 'https://images.unsplash.com/photo-1528207776546-365bb710ee93?auto=format&fit=crop&w=1200&q=85', 'calories' => 480, 'proteins' => 24, 'carbs' => 64, 'fats' => 14, 'type' => MealPostType::BREAKFAST, 'dietType' => DietType::VEGETARIAN, 'ingredients' => [ ['ingredient' => 'Flocons d’avoine', 'quantity' => 80, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Œufs', 'quantity' => 2, 'unit' => IngredientUnit::PIECE], ['ingredient' => 'Fruits rouges', 'quantity' => 120, 'unit' => IngredientUnit::GRAM], ], ], [ 'title' => 'Poulet rôti et légumes', 'caption' => 'Une assiette familiale, riche en protéines et pleine de couleurs.', 'imageUrl' => 'https://images.unsplash.com/photo-1532550907401-a500c9a57435?auto=format&fit=crop&w=1200&q=85', 'calories' => 590, 'proteins' => 52, 'carbs' => 46, 'fats' => 19, 'type' => MealPostType::DINNER, 'dietType' => DietType::OMNIVORE, 'ingredients' => [ ['ingredient' => 'Blanc de poulet', 'quantity' => 180, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Pommes de terre', 'quantity' => 220, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Légumes rôtis', 'quantity' => 180, 'unit' => IngredientUnit::GRAM], ], ], [ 'title' => 'Curry de pois chiches', 'caption' => 'Un curry végétal doux, crémeux et parfait pour le batch cooking.', 'imageUrl' => 'https://images.unsplash.com/photo-1603894584373-5ac82b2ae398?auto=format&fit=crop&w=1200&q=85', 'calories' => 540, 'proteins' => 21, 'carbs' => 72, 'fats' => 18, 'type' => MealPostType::DINNER, 'dietType' => DietType::VEGAN, 'ingredients' => [ ['ingredient' => 'Pois chiches', 'quantity' => 180, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Lait de coco', 'quantity' => 120, 'unit' => IngredientUnit::MILLILITER], ['ingredient' => 'Épinards', 'quantity' => 100, 'unit' => IngredientUnit::GRAM], ], ], [ 'title' => 'Pâtes au pesto maison', 'caption' => 'Un classique rapide avec tomates cerises, roquette et parmesan.', 'imageUrl' => 'https://images.unsplash.com/photo-1473093295043-cdd812d0e601?auto=format&fit=crop&w=1200&q=85', 'calories' => 670, 'proteins' => 25, 'carbs' => 88, 'fats' => 24, 'type' => MealPostType::LUNCH, 'dietType' => DietType::VEGETARIAN, 'ingredients' => [ ['ingredient' => 'Pâtes complètes', 'quantity' => 190, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Pesto', 'quantity' => 2, 'unit' => IngredientUnit::TABLESPOON], ['ingredient' => 'Tomates cerises', 'quantity' => 120, 'unit' => IngredientUnit::GRAM], ], ], [ 'title' => 'Toast avocat et œufs', 'caption' => 'Le brunch simple qui tient toute la matinée.', 'imageUrl' => 'https://images.unsplash.com/photo-1482049016688-2d3e1b311543?auto=format&fit=crop&w=1200&q=85', 'calories' => 510, 'proteins' => 26, 'carbs' => 42, 'fats' => 27, 'type' => MealPostType::BRUNCH, 'dietType' => DietType::VEGETARIAN, 'ingredients' => [ ['ingredient' => 'Pain complet', 'quantity' => 2, 'unit' => IngredientUnit::PIECE], ['ingredient' => 'Avocat', 'quantity' => 1, 'unit' => IngredientUnit::PIECE], ['ingredient' => 'Œufs', 'quantity' => 2, 'unit' => IngredientUnit::PIECE], ], ], [ 'title' => 'Tacos de poisson', 'caption' => 'Poisson grillé, chou croquant et sauce au yaourt citronnée.', 'imageUrl' => 'https://images.unsplash.com/photo-1551504734-5ee1c4a1479b?auto=format&fit=crop&w=1200&q=85', 'calories' => 560, 'proteins' => 36, 'carbs' => 61, 'fats' => 20, 'type' => MealPostType::DINNER, 'dietType' => DietType::PESCATARIAN, 'ingredients' => [ ['ingredient' => 'Poisson blanc', 'quantity' => 160, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Tortillas', 'quantity' => 3, 'unit' => IngredientUnit::PIECE], ['ingredient' => 'Chou rouge', 'quantity' => 100, 'unit' => IngredientUnit::GRAM], ], ], [ 'title' => 'Buddha bowl au tofu', 'caption' => 'Tofu croustillant, quinoa, légumes et sauce sésame.', 'imageUrl' => 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?auto=format&fit=crop&w=1200&q=85', 'calories' => 575, 'proteins' => 29, 'carbs' => 69, 'fats' => 22, 'type' => MealPostType::LUNCH, 'dietType' => DietType::VEGAN, 'ingredients' => [ ['ingredient' => 'Tofu', 'quantity' => 160, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Quinoa', 'quantity' => 150, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Brocoli', 'quantity' => 140, 'unit' => IngredientUnit::GRAM], ], ], [ 'title' => 'Steak et patate douce', 'caption' => 'Une assiette protéinée avec patate douce rôtie et haricots verts.', 'imageUrl' => 'https://images.unsplash.com/photo-1600891964092-4316c288032e?auto=format&fit=crop&w=1200&q=85', 'calories' => 710, 'proteins' => 49, 'carbs' => 65, 'fats' => 28, 'type' => MealPostType::DINNER, 'dietType' => DietType::OMNIVORE, 'ingredients' => [ ['ingredient' => 'Steak de bœuf', 'quantity' => 180, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Patate douce', 'quantity' => 240, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Haricots verts', 'quantity' => 150, 'unit' => IngredientUnit::GRAM], ], ], [ 'title' => 'Overnight oats banane', 'caption' => 'Préparé la veille avec banane, graines de chia et beurre de cacahuète.', 'imageUrl' => 'https://images.unsplash.com/photo-1517673400267-0251440c45dc?auto=format&fit=crop&w=1200&q=85', 'calories' => 445, 'proteins' => 18, 'carbs' => 62, 'fats' => 15, 'type' => MealPostType::BREAKFAST, 'dietType' => DietType::VEGETARIAN, 'ingredients' => [ ['ingredient' => 'Flocons d’avoine', 'quantity' => 70, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Banane', 'quantity' => 1, 'unit' => IngredientUnit::PIECE], ['ingredient' => 'Graines de chia', 'quantity' => 2, 'unit' => IngredientUnit::TEASPOON], ], ], [ 'title' => 'Salade grecque complète', 'caption' => 'Tomates, concombre, feta, olives et pain pita.', 'imageUrl' => 'https://images.unsplash.com/photo-1540420773420-3366772f4999?auto=format&fit=crop&w=1200&q=85', 'calories' => 495, 'proteins' => 19, 'carbs' => 47, 'fats' => 26, 'type' => MealPostType::LUNCH, 'dietType' => DietType::VEGETARIAN, 'ingredients' => [ ['ingredient' => 'Tomates', 'quantity' => 180, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Feta', 'quantity' => 80, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Pain pita', 'quantity' => 1, 'unit' => IngredientUnit::PIECE], ], ], [ 'title' => 'Ramen au poulet', 'caption' => 'Un bouillon chaud avec nouilles, poulet, œuf et légumes.', 'imageUrl' => 'https://images.unsplash.com/photo-1569718212165-3a8278d5f624?auto=format&fit=crop&w=1200&q=85', 'calories' => 640, 'proteins' => 42, 'carbs' => 78, 'fats' => 18, 'type' => MealPostType::DINNER, 'dietType' => DietType::OMNIVORE, 'ingredients' => [ ['ingredient' => 'Nouilles', 'quantity' => 180, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Poulet', 'quantity' => 140, 'unit' => IngredientUnit::GRAM], ['ingredient' => 'Œuf', 'quantity' => 1, 'unit' => IngredientUnit::PIECE], ], ], ]; } }