diff --git a/app/Http/Resources/Api/RecipeCollectionResource.php b/app/Http/Resources/Api/RecipeCollectionResource.php index b36f4325..5d62d2c9 100644 --- a/app/Http/Resources/Api/RecipeCollectionResource.php +++ b/app/Http/Resources/Api/RecipeCollectionResource.php @@ -29,6 +29,7 @@ public function toArray(Request $request): array return [ 'id' => $this->id, 'canonical_id' => $this->canonical_id, + 'variant' => $this->variant, 'url' => config('app.url') . '/' . $locale . '-' . resolve('current.country')->code . '/recipes/' . slugify($this->getTranslationWithAnyFallback('name', $locale)) . '-' . $this->id, 'name' => $this->getTranslationWithAnyFallback('name', $locale), diff --git a/app/Http/Resources/Api/RecipeResource.php b/app/Http/Resources/Api/RecipeResource.php index c008a014..f1cd3a19 100644 --- a/app/Http/Resources/Api/RecipeResource.php +++ b/app/Http/Resources/Api/RecipeResource.php @@ -34,6 +34,7 @@ public function toArray(Request $request): array return [ 'id' => $this->id, 'canonical_id' => $this->canonical_id, + 'variant' => $this->variant, 'url' => config('app.url') . '/' . $locale . '-' . resolve('current.country')->code . '/recipes/' . slugify($this->getTranslationWithAnyFallback('name', $locale)) . '-' . $this->id, 'name' => $this->getTranslationWithAnyFallback('name', $locale), diff --git a/app/Livewire/Portal/Docs/RecipesIndexDoc.php b/app/Livewire/Portal/Docs/RecipesIndexDoc.php index 81d6a42b..8f63e9ee 100644 --- a/app/Livewire/Portal/Docs/RecipesIndexDoc.php +++ b/app/Livewire/Portal/Docs/RecipesIndexDoc.php @@ -45,6 +45,7 @@ protected function responseFields(): array return [ ['name' => 'id', 'type' => 'integer', 'description' => 'Recipe ID'], ['name' => 'canonical_id', 'type' => 'integer|null', 'description' => 'ID of the original recipe if this is a variant'], + ['name' => 'variant', 'type' => 'boolean', 'description' => 'Whether this recipe is a variant of another recipe'], ['name' => 'url', 'type' => 'string', 'description' => 'URL to recipe on website'], ['name' => 'name', 'type' => 'string', 'description' => 'Recipe name (localized)'], ['name' => 'headline', 'type' => 'string', 'description' => 'Short description (localized)'], diff --git a/app/Livewire/Portal/Docs/RecipesShowDoc.php b/app/Livewire/Portal/Docs/RecipesShowDoc.php index aeebd18d..2845bf20 100644 --- a/app/Livewire/Portal/Docs/RecipesShowDoc.php +++ b/app/Livewire/Portal/Docs/RecipesShowDoc.php @@ -29,6 +29,7 @@ protected function responseFields(): array return [ ['name' => 'id', 'type' => 'integer', 'description' => 'Recipe ID'], ['name' => 'canonical_id', 'type' => 'integer|null', 'description' => 'ID of the original recipe if this is a variant'], + ['name' => 'variant', 'type' => 'boolean', 'description' => 'Whether this recipe is a variant of another recipe'], ['name' => 'url', 'type' => 'string', 'description' => 'URL to recipe on website'], ['name' => 'name', 'type' => 'string', 'description' => 'Recipe name (localized)'], ['name' => 'headline', 'type' => 'string', 'description' => 'Short description (localized)'], diff --git a/app/Livewire/Portal/Stats/RecipeStats.php b/app/Livewire/Portal/Stats/RecipeStats.php index d9202710..58bb2f1a 100644 --- a/app/Livewire/Portal/Stats/RecipeStats.php +++ b/app/Livewire/Portal/Stats/RecipeStats.php @@ -168,14 +168,14 @@ public function dataHealth(): array } /** - * Get canonical recipe statistics. + * Get variant recipe statistics. * - * @return array{total_canonical: int, recipes_with_canonical: int, unique_canonical_parents: int, canonical_percentage: float} + * @return array{total_variants: int, unique_canonical_parents: int, variant_percentage: float} */ #[Computed] - public function canonicalStats(): array + public function variantStats(): array { - return $this->statistics()->canonicalStats(); + return $this->statistics()->variantStats(); } public function render(): View diff --git a/app/Livewire/Web/Auth/ResetPassword.php b/app/Livewire/Web/Auth/ResetPassword.php index 2756cfa4..ca94df61 100644 --- a/app/Livewire/Web/Auth/ResetPassword.php +++ b/app/Livewire/Web/Auth/ResetPassword.php @@ -16,7 +16,9 @@ use Illuminate\Support\Str; use Illuminate\Validation\Rules\Password as PasswordRule; use Illuminate\Validation\ValidationException; +use Livewire\Attributes\Layout; +#[Layout('web::components.layouts.localized')] class ResetPassword extends AbstractComponent { use WithLocalizedContextTrait; diff --git a/app/Livewire/Web/Recipes/RecipeIndex.php b/app/Livewire/Web/Recipes/RecipeIndex.php index 4156da4f..72dbb18b 100644 --- a/app/Livewire/Web/Recipes/RecipeIndex.php +++ b/app/Livewire/Web/Recipes/RecipeIndex.php @@ -431,7 +431,7 @@ public function recipes(): LengthAwarePaginator ->when($menuRecipeIds !== [], fn (Builder $query) => $query->whereIn('id', $menuRecipeIds)) ->when($this->search !== '', fn (Builder $query): Builder => $this->applySearchFilter($query)) ->when($this->filterHasPdf, fn (Builder $query) => $query->where('has_pdf', true)) - ->when(! $this->filterShowCanonical, fn (Builder $query) => $query->whereNull('canonical_id')) + ->when(! $this->filterShowCanonical, fn (Builder $query) => $query->where('variant', false)) ->when($this->excludedAllergenIds !== [], fn (Builder $query) => $query->whereDoesntHave( 'allergens', fn (Builder $allergenQuery) => $allergenQuery->whereIn('allergens.id', $this->excludedAllergenIds) diff --git a/app/Livewire/Web/Recipes/RecipeRandom.php b/app/Livewire/Web/Recipes/RecipeRandom.php index 66041015..2a23e85b 100644 --- a/app/Livewire/Web/Recipes/RecipeRandom.php +++ b/app/Livewire/Web/Recipes/RecipeRandom.php @@ -25,7 +25,7 @@ public function randomRecipes(): Collection return Recipe::where('country_id', $this->countryId) ->when($this->search !== '', fn (Builder $query): Builder => $this->applySearchFilter($query)) ->when($this->filterHasPdf, fn (Builder $query) => $query->where('has_pdf', true)) - ->unless($this->filterShowCanonical, fn (Builder $query) => $query->whereNull('canonical_id')) + ->unless($this->filterShowCanonical, fn (Builder $query) => $query->where('variant', false)) ->when($this->excludedAllergenIds !== [], fn (Builder $query) => $query->whereDoesntHave( 'allergens', fn (Builder $allergenQuery) => $allergenQuery->whereIn('allergens.id', $this->excludedAllergenIds) diff --git a/app/Models/Recipe.php b/app/Models/Recipe.php index e8396e5f..0cb53129 100644 --- a/app/Models/Recipe.php +++ b/app/Models/Recipe.php @@ -246,21 +246,13 @@ protected function hellofreshUrl(): Attribute return Attribute::get(fn (): ?string => $this->buildHellofreshUrl()); } - /** - * Check if this recipe is a canonical recipe (has a parent). - */ - public function isCanonical(): bool - { - return $this->canonical_id !== null; - } - /** * Build the HelloFresh URL. */ protected function buildHellofreshUrl(): ?string { - // Canonical recipes often lead to 404 pages on HelloFresh - if ($this->isCanonical()) { + // Variant recipes often lead to 404 pages on HelloFresh + if ($this->variant) { return null; } diff --git a/app/Services/Portal/StatisticsService.php b/app/Services/Portal/StatisticsService.php index 89f9469b..0077f7a6 100644 --- a/app/Services/Portal/StatisticsService.php +++ b/app/Services/Portal/StatisticsService.php @@ -41,7 +41,7 @@ class StatisticsService 'portal_recipes_per_month', 'portal_avg_prep_times', 'portal_data_health', - 'portal_canonical_stats', + 'portal_variant_stats', ]; /** @@ -72,7 +72,7 @@ public function warmCache(): void $this->recipesPerMonth(); $this->avgPrepTimesByCountry(); $this->dataHealth(); - $this->canonicalStats(); + $this->variantStats(); } /** @@ -104,7 +104,7 @@ public function countryStats(): Collection /** @var Collection */ return Cache::remember('portal_country_stats', $this->cacheTtl, static fn (): Collection => Country::where('active', true) ->withCount('menus') - ->withCount(['recipes as variants_count' => fn (Builder $query) => $query->whereNotNull('canonical_id')]) + ->withCount(['recipes as variants_count' => fn (Builder $query) => $query->where('variant', true)]) ->get()); } @@ -316,25 +316,24 @@ public function dataHealth(): array } /** - * Get canonical recipe statistics. + * Get variant recipe statistics. * - * @return array{total_canonical: int, recipes_with_canonical: int, unique_canonical_parents: int, canonical_percentage: float} + * @return array{total_variants: int, unique_canonical_parents: int, variant_percentage: float} */ - public function canonicalStats(): array + public function variantStats(): array { - /** @var array{total_canonical: int, recipes_with_canonical: int, unique_canonical_parents: int, canonical_percentage: float} */ - return Cache::remember('portal_canonical_stats', $this->cacheTtl, static function (): array { + /** @var array{total_variants: int, unique_canonical_parents: int, variant_percentage: float} */ + return Cache::remember('portal_variant_stats', $this->cacheTtl, static function (): array { $total = Recipe::count(); - $recipesWithCanonical = Recipe::whereNotNull('canonical_id')->count(); + $totalVariants = Recipe::where('variant', true)->count(); $uniqueCanonicalParents = Recipe::whereNotNull('canonical_id') ->distinct('canonical_id') ->count('canonical_id'); return [ - 'total_canonical' => $recipesWithCanonical, - 'recipes_with_canonical' => $recipesWithCanonical, + 'total_variants' => $totalVariants, 'unique_canonical_parents' => $uniqueCanonicalParents, - 'canonical_percentage' => $total > 0 ? round(($recipesWithCanonical / $total) * 100, 1) : 0.0, + 'variant_percentage' => $total > 0 ? round(($totalVariants / $total) * 100, 1) : 0.0, ]; }); } diff --git a/resources/views/portal/livewire/stats/recipe-stats.blade.php b/resources/views/portal/livewire/stats/recipe-stats.blade.php index e6ecbe99..8d24a5d8 100644 --- a/resources/views/portal/livewire/stats/recipe-stats.blade.php +++ b/resources/views/portal/livewire/stats/recipe-stats.blade.php @@ -107,12 +107,12 @@
- Canonical Recipes - {{ Number::format($this->canonicalStats['total_canonical']) }} + Variant Recipes + {{ Number::format($this->variantStats['total_variants']) }}
- {{ $this->canonicalStats['canonical_percentage'] }}% of all recipes - {{ Number::format($this->canonicalStats['unique_canonical_parents']) }} unique parents + {{ $this->variantStats['variant_percentage'] }}% of all recipes + {{ Number::format($this->variantStats['unique_canonical_parents']) }} unique parents
diff --git a/tests/Feature/Livewire/Recipes/RecipeIndexTest.php b/tests/Feature/Livewire/Recipes/RecipeIndexTest.php index d18a320c..4d2cc431 100644 --- a/tests/Feature/Livewire/Recipes/RecipeIndexTest.php +++ b/tests/Feature/Livewire/Recipes/RecipeIndexTest.php @@ -5,7 +5,6 @@ namespace Tests\Feature\Livewire\Recipes; use App\Enums\IngredientMatchModeEnum; -use App\Enums\RecipeSortEnum; use App\Enums\ViewModeEnum; use App\Livewire\Web\Recipes\RecipeIndex; use App\Models\Allergen; @@ -324,16 +323,6 @@ public function it_persists_view_mode_to_session(): void $this->assertSame(ViewModeEnum::List->value, session('view_mode')); } - #[Test] - public function it_persists_sort_to_session(): void - { - Livewire::test(RecipeIndex::class) - ->set('sortBy', RecipeSortEnum::OldestFirst->value); - - $key = sprintf('recipe_filter_%d_sort', $this->country->id); - $this->assertSame(RecipeSortEnum::OldestFirst->value, session($key)); - } - #[Test] public function it_can_filter_by_menu(): void { diff --git a/tests/Unit/Jobs/ImportRecipeJobTest.php b/tests/Unit/Jobs/ImportRecipeJobTest.php index 1be1c28a..1ab57e59 100644 --- a/tests/Unit/Jobs/ImportRecipeJobTest.php +++ b/tests/Unit/Jobs/ImportRecipeJobTest.php @@ -75,6 +75,7 @@ protected function createValidRecipeData(): array 'cuisines' => [], 'utensils' => [], 'label' => null, + 'canonical' => '', 'createdAt' => now()->toIso8601String(), 'updatedAt' => now()->toIso8601String(), ];