diff --git a/CHANGELOG.md b/CHANGELOG.md index b99f1252..9fbdc728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# v0.4.0 +- Refactor image uploading and handling +- Updates for some base ingredients +- Add more popular cocktails +- Finish cocktail ingredient substitutes endpoints +- Finish glass type endpoints + # v0.3.0 - Move "Uncategorized" ingredient category to id: 1 - Cocktail save/update methods now save glass type diff --git a/app/Console/Commands/BarExportCocktails.php b/app/Console/Commands/BarExportCocktails.php index b2b78218..2557f624 100644 --- a/app/Console/Commands/BarExportCocktails.php +++ b/app/Console/Commands/BarExportCocktails.php @@ -30,7 +30,7 @@ class BarExportCocktails extends Command public function handle() { $dump = []; - Cocktail::with('tags', 'ingredients.ingredient')->orderBy('name')->chunk(200, function ($cocktails) use (&$dump) { + Cocktail::with('tags', 'ingredients.ingredient')->orderBy('created_at')->chunk(200, function ($cocktails) use (&$dump) { foreach ($cocktails as $cocktail) { $dump[] = [ 'name' => $cocktail->name, @@ -40,6 +40,7 @@ public function handle() 'source' => $cocktail->source, 'image_copyright' => $cocktail->images->first()->copyright ?? null, 'tags' => $cocktail->tags->pluck('name')->toArray(), + 'glass' => $cocktail->glass->name, 'ingredients' => $cocktail->ingredients->map(function ($cIng) { return [ 'amount' => $cIng->amount, diff --git a/app/Console/Commands/OpenBar.php b/app/Console/Commands/OpenBar.php index 489ed83a..01691bfd 100644 --- a/app/Console/Commands/OpenBar.php +++ b/app/Console/Commands/OpenBar.php @@ -162,8 +162,6 @@ public function handle() Ingredient::create(['name' => 'Triple Sec', 'ingredient_category_id' => $liqueurs->id, 'strength' => 40.0, 'description' => 'Triple sec is usually made from orange peels steeped in a spirit derived from sugar beet due to its neutral flavor. Oranges are then harvested when their skin is still green and they have not fully ripened, so the essential oils remain in the skin and not the flesh of the fruit. This spirit is then redistilled and mixed with more neutral spirit, water, and powdered beet sugar resulting in the final liqueur. This process creates a spirit that has a very strong and distinct orange flavor.', 'color' => '#ffffff', 'origin' => 'France', 'user_id' => 1]); Ingredient::create(['name' => 'Maraschino', 'ingredient_category_id' => $liqueurs->id, 'strength' => 32.0, 'description' => 'Liqueur obtained from the distillation of Marasca cherries. The small, slightly sour fruit of the Tapiwa cherry tree, which grows wild along parts of the Dalmatian coast in Croatia, lends the liqueur its unique aroma.', 'color' => '#ffffff', 'origin' => 'Croatia', 'user_id' => 1]); Ingredient::create(['name' => 'Galliano', 'ingredient_category_id' => $liqueurs->id, 'strength' => 42.3, 'description' => 'Galliano is sweet with vanilla-anise flavour and subtle citrus and woodsy herbal undernotes.', 'color' => '#caa701', 'origin' => 'Italy', 'user_id' => 1]); - Ingredient::create(['name' => 'Orange Curaçao', 'ingredient_category_id' => $liqueurs->id, 'strength' => 20.0, 'description' => 'Liqueur flavored with the dried peel of the bitter orange laraha, a citrus fruit grown on the Dutch island of Curaçao.', 'color' => '#edaa53', 'origin' => 'Netherlands', 'user_id' => 1]); - Ingredient::create(['name' => 'Blue Curaçao', 'ingredient_category_id' => $liqueurs->id, 'strength' => 20.0, 'description' => 'Liqueur flavored with the dried peel of the bitter orange laraha, a citrus fruit grown on the Dutch island of Curaçao.', 'color' => '#0192fe', 'origin' => 'Netherlands', 'user_id' => 1]); Ingredient::create(['name' => 'Chambord', 'ingredient_category_id' => $liqueurs->id, 'strength' => 16.5, 'description' => 'Raspberry liqueur modelled after a liqueur produced in the Loire Valley of France during the late 17th century.', 'color' => '#6f1123', 'origin' => 'Worldwide', 'user_id' => 1]); Ingredient::create(['name' => 'Falernum', 'ingredient_category_id' => $liqueurs->id, 'strength' => 11.0, 'description' => 'Liqueur with flavors of ginger, lime, and almond, and frequently cloves or allspice. It may be thought of as a spicier version of orgeat syrup.', 'color' => '#f4f2e5', 'origin' => 'Caribbean', 'user_id' => 1]); Ingredient::create(['name' => 'Green Chartreuse', 'ingredient_category_id' => $liqueurs->id, 'strength' => 55.0, 'description' => 'Green Chartreuse is a naturally green liqueur made from 130 herbs and other plants macerated in alcohol and steeped for about eight hours.', 'color' => '#85993a', 'origin' => 'France', 'user_id' => 1]); @@ -176,6 +174,11 @@ public function handle() Ingredient::create(['name' => 'Ouzo', 'ingredient_category_id' => $liqueurs->id, 'strength' => 35.0, 'description' => 'Dry anise-flavored aperitif that is widely consumed in Greece.', 'color' => '#ffffff', 'origin' => 'Greece', 'user_id' => 1]); Ingredient::create(['name' => 'Passoã', 'ingredient_category_id' => $liqueurs->id, 'strength' => 17.0, 'description' => 'Liqueur with passion fruit being the main ingredient.', 'color' => '#ea5f4c', 'origin' => 'France', 'user_id' => 1]); Ingredient::create(['name' => 'Fernet Branca', 'ingredient_category_id' => $liqueurs->id, 'strength' => 39.0, 'description' => 'Fernet Branca is a bittersweet, herbal liqueur made with a number of different herbs and spices, including myrrh, rhubarb, chamomile, cardamom, aloe, and gentian root.', 'origin' => 'Italy', 'user_id' => 1]); + Ingredient::create(['name' => 'Baileys Irish Cream', 'ingredient_category_id' => $liqueurs->id, 'strength' => 17.0, 'description' => 'Baileys Irish Cream is an Irish cream liqueur, an alcoholic drink flavoured with cream, cocoa and Irish whiskey. It is made by Diageo at Nangor Road, in Dublin, Ireland and in Mallusk, Northern Ireland. It is the original Irish cream, invented by a team headed by Tom Jago in 1971 for Gilbeys of Ireland.', 'origin' => 'Ireland', 'user_id' => 1]); + + $curacao = Ingredient::create(['name' => 'Orange Curaçao', 'ingredient_category_id' => $liqueurs->id, 'strength' => 20.0, 'description' => 'Liqueur flavored with the dried peel of the bitter orange laraha, a citrus fruit grown on the Dutch island of Curaçao. Curaçao is used by liqueur makers the world over as a generic name for orange-flavoured liqueurs.', 'color' => '#edaa53', 'origin' => 'Netherlands', 'user_id' => 1]); + Ingredient::create(['name' => 'Dry Curaçao', 'parent_ingredient_id' => $curacao->id, 'ingredient_category_id' => $liqueurs->id, 'strength' => 40.0, 'description' => 'Cognac Ferrand\'s innovative owner Alexandre Gabriel, followed an old recipe and modified it to create this curaçao. While Curaçao and sweet oranges are the main ingredients, vanilla, prunes and lemon peel are amongst the other botanicals called for in the old recipe.', 'origin' => 'Italy', 'user_id' => 1]); + Ingredient::create(['name' => 'Blue Curaçao', 'parent_ingredient_id' => $curacao->id, 'ingredient_category_id' => $liqueurs->id, 'strength' => 20.0, 'description' => 'Liqueur flavored with the dried peel of the bitter orange laraha, a citrus fruit grown on the Dutch island of Curaçao.', 'color' => '#0192fe', 'origin' => 'Netherlands', 'user_id' => 1]); // Juices Ingredient::create(['name' => 'Lemon juice', 'ingredient_category_id' => $juices->id, 'strength' => 0.0, 'description' => 'Freshly squeezed lemon juice.', 'color' => '#f3efda', 'user_id' => 1]); @@ -251,19 +254,23 @@ public function handle() Ingredient::create(['name' => 'Ginger syrup', 'ingredient_category_id' => $syrups->id, 'description' => 'Syrup made from ginger root.', 'color' => '#c6972c', 'user_id' => 1]); // Wines - Ingredient::create(['name' => 'Sweet Vermouth', 'ingredient_category_id' => $wines->id, 'strength' => 18.0, 'description' => 'Aromatized fortified wine.', 'color' => '#8e4201', 'user_id' => 1]); - Ingredient::create(['name' => 'Dry Vermouth', 'ingredient_category_id' => $wines->id, 'strength' => 18.0, 'description' => 'Aromatized fortified wine.', 'color' => '#ffffff', 'user_id' => 1]); - Ingredient::create(['name' => 'White wine', 'ingredient_category_id' => $wines->id, 'strength' => 11.0, 'description' => 'Wine is an alcoholic drink typically made from fermented grapes.', 'color' => '#f6e1b0', 'user_id' => 1]); - Ingredient::create(['name' => 'Red wine', 'ingredient_category_id' => $wines->id, 'strength' => 11.0, 'description' => 'Red wine is a type of wine made from dark-colored grape varieties.', 'color' => '#801212', 'user_id' => 1]); - Ingredient::create(['name' => 'Prosecco', 'ingredient_category_id' => $wines->id, 'strength' => 11.0, 'description' => 'Sparkling wine made from Prosecco grapes.', 'color' => '#a57600', 'user_id' => 1]); - Ingredient::create(['name' => 'Champagne', 'ingredient_category_id' => $wines->id, 'strength' => 12.0, 'description' => 'Sparkling wine.', 'color' => '#f6e1b0', 'user_id' => 1]); - Ingredient::create(['name' => 'Lillet Blanc', 'ingredient_category_id' => $wines->id, 'strength' => 17.0, 'description' => 'Aromatized sweet wine.', 'color' => '#f7ec77', 'user_id' => 1]); + Ingredient::create(['name' => 'Sweet Vermouth', 'ingredient_category_id' => $wines->id, 'strength' => 18.0, 'description' => 'Aromatized fortified wine.', 'color' => '#8e4201', 'user_id' => 1, 'origin' => 'Worldwide']); + Ingredient::create(['name' => 'Dry Vermouth', 'ingredient_category_id' => $wines->id, 'strength' => 18.0, 'description' => 'Aromatized fortified wine.', 'color' => '#ffffff', 'user_id' => 1, 'origin' => 'Worldwide']); + Ingredient::create(['name' => 'White wine', 'ingredient_category_id' => $wines->id, 'strength' => 11.0, 'description' => 'Wine is an alcoholic drink typically made from fermented grapes.', 'color' => '#f6e1b0', 'user_id' => 1, 'origin' => 'Worldwide']); + Ingredient::create(['name' => 'Red wine', 'ingredient_category_id' => $wines->id, 'strength' => 11.0, 'description' => 'Red wine is a type of wine made from dark-colored grape varieties.', 'color' => '#801212', 'user_id' => 1, 'origin' => 'Worldwide']); + Ingredient::create(['name' => 'Prosecco', 'ingredient_category_id' => $wines->id, 'strength' => 11.0, 'description' => 'Sparkling wine made from Prosecco grapes.', 'color' => '#a57600', 'user_id' => 1, 'origin' => 'Italy']); + Ingredient::create(['name' => 'Champagne', 'ingredient_category_id' => $wines->id, 'strength' => 12.0, 'description' => 'Sparkling wine.', 'color' => '#f6e1b0', 'user_id' => 1, 'origin' => 'France']); + Ingredient::create(['name' => 'Lillet Blanc', 'ingredient_category_id' => $wines->id, 'strength' => 17.0, 'description' => 'Aromatized sweet wine.', 'color' => '#f7ec77', 'user_id' => 1, 'origin' => 'France']); + Ingredient::create(['name' => 'Dry Sherry', 'ingredient_category_id' => $wines->id, 'strength' => 17.0, 'description' => 'Fortified wine made from white grapes that are grown near the city of Jerez de la Frontera in Andalusia, Spain.', 'color' => '#8c4122', 'user_id' => 1, 'origin' => 'Spain']); $this->info('Attaching images to ingredients...'); // Create image for every ingredient $ingredients = Ingredient::all(); foreach ($ingredients as $ing) { + if (!file_exists(storage_path('uploads/ingredients/' . Str::slug($ing->name) . '.png'))) { + continue; + } $image = new Image(); $image->file_path = 'ingredients/' . Str::slug($ing->name) . '.png'; $image->file_extension = 'png'; @@ -278,7 +285,7 @@ public function handle() $this->info('Finding some cocktail recipes...'); $this->importCocktailsFromJson(resource_path('/data/iba_cocktails_v0.1.0.yml')); - $this->importCocktailsFromJson(resource_path('/data/popular_cocktails_v0.3.0.yml')); + $this->importCocktailsFromJson(resource_path('/data/popular_cocktails.yml')); Artisan::call('scout:import', ['model' => "Kami\Cocktail\Models\Cocktail"]); Artisan::call('scout:import', ['model' => "Kami\Cocktail\Models\Ingredient"]); @@ -361,13 +368,18 @@ private function importCocktailsFromJson(string $sourcePath) $cocktailIng->optional = $sIngredient['optional']; $cocktailIng->save(); - // if (isset($sIngredient['substitutes'])) { - // foreach ($sIngredient['substitutes'] ?? [] as $subName) { - // $substitute = new CocktailIngredientSubstitute(); - // $substitute->ingredient_id = $dbIngredients->filter(fn ($item) => $item->name == strtolower($subName))->first()->id ?? null; - // $cocktailIng->substitutes()->save($substitute); - // } - // } + if (isset($sIngredient['substitutes'])) { + foreach ($sIngredient['substitutes'] ?? [] as $subName) { + $foundIng = $dbIngredients->filter(fn ($item) => $item->name == strtolower($subName))->first()->id ?? null; + if (!$foundIng) { + continue; + } + + $substitute = new CocktailIngredientSubstitute(); + $substitute->ingredient_id = $foundIng; + $cocktailIng->substitutes()->save($substitute); + } + } } $cocktail->refresh(); diff --git a/app/Console/Commands/TestScrap.php b/app/Console/Commands/TestScrap.php new file mode 100644 index 00000000..4315162b --- /dev/null +++ b/app/Console/Commands/TestScrap.php @@ -0,0 +1,41 @@ +toArray()); + + return Command::SUCCESS; + } +} diff --git a/app/Http/Controllers/CocktailController.php b/app/Http/Controllers/CocktailController.php index 6e4e4b93..e35a0a94 100644 --- a/app/Http/Controllers/CocktailController.php +++ b/app/Http/Controllers/CocktailController.php @@ -19,6 +19,7 @@ class CocktailController extends Controller * - Paginated by 15 items * Optional query strings: * - user_id -> Filter by user id + * - favorites -> Filter by user favorites */ public function index(Request $request) { @@ -28,6 +29,12 @@ public function index(Request $request) $cocktails->where('user_id', $request->get('user_id')); } + if ($request->has('favorites')) { + $cocktails->whereIn('id', function ($query) use ($request) { + $query->select('cocktail_id')->from('cocktail_favorites')->where('user_id', $request->user()->id); + }); + } + return CocktailResource::collection($cocktails->paginate(15)); } @@ -142,7 +149,7 @@ public function userShelf(CocktailService $cocktailService, Request $request) /** * Favorite a cocktail by id */ - public function favorite(CocktailService $cocktailService, Request $request, int $id) + public function toggleFavorite(CocktailService $cocktailService, Request $request, int $id) { $isFavorite = $cocktailService->toggleFavorite($request->user(), $id); diff --git a/app/Http/Controllers/GlassController.php b/app/Http/Controllers/GlassController.php new file mode 100644 index 00000000..70c70720 --- /dev/null +++ b/app/Http/Controllers/GlassController.php @@ -0,0 +1,25 @@ +get(); + + return GlassResource::collection($glasses); + } + + public function show(int $id) + { + $glass = Glass::findOrFail($id); + + return new GlassResource($glass); + } +} diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index 09ceccf0..d0e66bca 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -3,6 +3,7 @@ namespace Kami\Cocktail\Http\Controllers; +use Illuminate\Http\Request; use Kami\Cocktail\Models\Image; use Kami\Cocktail\Services\ImageService; use Kami\Cocktail\Http\Requests\ImageRequest; @@ -25,6 +26,13 @@ public function store(ImageService $imageservice, ImageRequest $request) return ImageResource::collection($images); } + public function update(int $id, ImageService $imageservice, Request $request) + { + $image = $imageservice->updateImage($id, null, $request->input('copyright')); + + return new ImageResource($image); + } + public function delete(int $id) { Image::findOrFail($id)->delete(); diff --git a/app/Http/Resources/CocktailResource.php b/app/Http/Resources/CocktailResource.php index f2f60f67..f2c9efcc 100644 --- a/app/Http/Resources/CocktailResource.php +++ b/app/Http/Resources/CocktailResource.php @@ -26,9 +26,8 @@ public function toArray($request) 'garnish' => e($this->garnish), 'description' => e($this->description), 'source' => $this->source, - 'image_copyright' => $this->images->first()->copyright ?? null, - 'image_url' => $this->getImageUrl(), - 'image_id' => $this->images->first()->id ?? null, + 'main_image_id' => $this->images->first()->id ?? null, + 'images' => ImageResource::collection($this->images), 'tags' => $this->tags->pluck('name'), 'user_id' => $this->user_id, 'glass' => new GlassResource($this->whenLoaded('glass')), diff --git a/app/Http/Resources/ImageResource.php b/app/Http/Resources/ImageResource.php index 7abfa23e..82b8b9d5 100644 --- a/app/Http/Resources/ImageResource.php +++ b/app/Http/Resources/ImageResource.php @@ -3,6 +3,7 @@ namespace Kami\Cocktail\Http\Resources; +use Illuminate\Support\Facades\Storage; use Illuminate\Http\Resources\Json\JsonResource; /** @@ -21,6 +22,7 @@ public function toArray($request) return [ 'id' => $this->id, 'file_path' => $this->file_path, + 'url' => $this->getImageUrl(), 'copyright' => $this->copyright, 'last_modified' => $this->updated_at, ]; diff --git a/app/Http/Resources/IngredientResource.php b/app/Http/Resources/IngredientResource.php index e6c0f001..8c1bdecd 100644 --- a/app/Http/Resources/IngredientResource.php +++ b/app/Http/Resources/IngredientResource.php @@ -25,7 +25,8 @@ public function toArray($request) 'strength' => $this->strength, 'description' => $this->description, 'origin' => $this->origin, - 'image_url' => $this->getImageUrl(), + 'main_image_id' => $this->images->first()->id ?? null, + 'images' => ImageResource::collection($this->images), 'ingredient_category_id' => $this->ingredient_category_id, 'color' => $this->color, 'category' => new IngredientCategoryResource($this->category), @@ -39,8 +40,8 @@ public function toArray($request) ]; })->toArray(); }), - 'cocktails' => $this->whenLoaded('cocktails', function () { - return $this->cocktails->map(function ($c) { + 'cocktails' => $this->when($this->relationLoaded('cocktails') || $this->relationLoaded('cocktailIngredientSubstitutes'), function () { + return $this->cocktails->merge($this->cocktailsAsSubstituteIngredient())->map(function ($c) { return [ 'id' => $c->id, 'slug' => $c->slug, diff --git a/app/Models/Cocktail.php b/app/Models/Cocktail.php index 49588028..8f7e124d 100644 --- a/app/Models/Cocktail.php +++ b/app/Models/Cocktail.php @@ -18,7 +18,6 @@ class Cocktail extends Model use HasFactory, Searchable, HasImages, HasSlug; private $appImagesDir = 'cocktails/'; - private $missingImageFileName = 'no-image.jpg'; // TODO: WEBP protected static function booted() { @@ -72,7 +71,7 @@ public function toSiteSearchArray() 'id' => $this->id, 'slug' => $this->slug, 'name' => $this->name, - 'image_url' => $this->getImageUrl(), + 'image_url' => $this->getMainImageUrl(), 'type' => 'cocktail', ]; } @@ -87,7 +86,7 @@ public function toSearchableArray(): array 'description' => $this->description, 'source' => $this->source, 'garnish' => $this->garnish, - 'image_url' => $this->getImageUrl(), + 'image_url' => $this->getMainImageUrl(), 'short_ingredients' => $this->ingredients->pluck('ingredient.name'), 'user_id' => $this->user_id, 'tags' => $this->tags->pluck('name'), diff --git a/app/Models/HasImages.php b/app/Models/HasImages.php index aa2bb24b..5654fac7 100644 --- a/app/Models/HasImages.php +++ b/app/Models/HasImages.php @@ -15,22 +15,9 @@ public function images(): MorphMany return $this->morphMany(Image::class, 'imageable'); } - public function latestImageFilePath(): ?string + public function getMainImageUrl(): ?string { - return $this->images->first()->file_path ?? null; - } - - public function getImageUrl(): string - { - $disk = Storage::disk('app_images'); - - $filePath = $this->latestImageFilePath(); - - if (!$filePath || !$disk->exists($filePath)) { - return $disk->url($this->appImagesDir . $this->missingImageFileName); - } - - return $disk->url($filePath); + return $this->images->first()?->getImageUrl(); } public function deleteImages(): void diff --git a/app/Models/Image.php b/app/Models/Image.php index 82a990fe..a0344011 100644 --- a/app/Models/Image.php +++ b/app/Models/Image.php @@ -23,6 +23,15 @@ public function delete() parent::delete(); } + public function getImageUrl(): ?string + { + if (!$this->file_path) { + return null; + } + + return Storage::disk('app_images')->url($this->file_path); + } + public function imageable(): MorphTo { return $this->morphTo(); diff --git a/app/Models/Ingredient.php b/app/Models/Ingredient.php index 42a6fa19..ffec177a 100644 --- a/app/Models/Ingredient.php +++ b/app/Models/Ingredient.php @@ -18,7 +18,6 @@ class Ingredient extends Model use HasFactory, Searchable, HasImages, HasSlug; private $appImagesDir = 'ingredients/'; - private $missingImageFileName = 'no-image.png'; // TODO: WEBP protected $fillable = [ 'name', @@ -74,6 +73,16 @@ public function parentIngredient(): BelongsTo return $this->belongsTo(Ingredient::class, 'parent_ingredient_id', 'id'); } + public function cocktailsAsSubstituteIngredient() + { + return $this->cocktailIngredientSubstitutes->pluck('cocktailIngredient.cocktail'); + } + + public function cocktailIngredientSubstitutes(): HasMany + { + return $this->hasMany(CocktailIngredientSubstitute::class); + } + public function getAllRelatedIngredients() { // This creates "Related" group of the ingredients "on-the-fly" @@ -102,7 +111,7 @@ public function toSiteSearchArray() 'id' => $this->id, 'slug' => $this->slug, 'name' => $this->name, - 'image_url' => $this->getImageUrl(), + 'image_url' => $this->getMainImageUrl(), 'type' => 'ingredient', ]; } @@ -113,7 +122,7 @@ public function toSearchableArray(): array 'id' => $this->id, 'slug' => $this->slug, 'name' => $this->name, - 'image_url' => $this->getImageUrl(), + 'image_url' => $this->getMainImageUrl(), 'description' => $this->description, 'category' => $this->category->name, ]; diff --git a/app/Scraper/Scraper.php b/app/Scraper/Scraper.php new file mode 100644 index 00000000..e0146a91 --- /dev/null +++ b/app/Scraper/Scraper.php @@ -0,0 +1,124 @@ +request('GET', $url); + $this->crawler = new Crawler($browser->getResponse()->getContent()); + } + + public function name() + { + return $this->crawler->filter('.recipe__header-title')->first()->text(); + } + + public function description() + { + return $this->crawler->filter('.recipe__header-subtitle')->first()->text(); + } + + public function source() + { + return $this->url; + } + + public function instructions() + { + $result = ''; + + $step = 1; + $this->crawler->filter('.recipe__recipe ol')->first()->filter('li')->each(function ($node) use (&$result, &$step) { + $result .= $step . ". " . $node->text() . "\n"; + $step++; + }); + + return $result; + } + + public function tags() + { + return null; + } + + public function glass() + { + $glassTag = $this->crawler->filter('.recipe__header-titles-and-icons .recipe__tag-icons a')->last()->attr('href'); + + return str_replace( + '-', + ' ', + str_replace('/tags/', '', $glassTag) + ); + } + + public function ingredients() + { + $result = []; + + $this->crawler->filter('.recipe__recipe ul')->first()->filter('li')->each(function ($node) use (&$result) { + $isGarnish = $node->filter('.amount .unit')->count() === 0; + + if ($isGarnish) { + return; + } + + $unit = $node->filter('.amount .unit')->text(); + $amount = iconv('','US//TRANSLIT', str_replace($unit, '', $node->filter('.amount')->text())); + + if ($unit === 'oz') { + $numbers = explode('/', $amount); + + $denominator = $numbers[1] ?? 1; + $amount = ((int) $numbers[0] / $denominator) * 30; + $unit = 'ml'; + } + + $result[] = [ + 'amount' => $amount, + 'unit' => $unit, + 'name' => $node->filter('.ingredient a')->first()->text(), + 'optional' => false, + ]; + }); + + return $result; + } + + public function garnish() + { + return null; + } + + public function toArray() + { + return [ + 'name' => $this->name(), + 'description' => $this->description(), + 'source' => $this->source(), + 'glass' => $this->glass(), + 'instructions' => $this->instructions(), + 'garnish' => $this->garnish(), + 'tags' => $this->tags(), + 'ingredients' => $this->ingredients(), + ]; + } +} diff --git a/app/Services/CocktailService.php b/app/Services/CocktailService.php index d611d0a9..b8708c04 100644 --- a/app/Services/CocktailService.php +++ b/app/Services/CocktailService.php @@ -8,6 +8,7 @@ use Illuminate\Log\LogManager; use Kami\Cocktail\Models\User; use Kami\Cocktail\Models\Image; +use Illuminate\Support\Facades\DB; use Kami\Cocktail\Models\Cocktail; use Illuminate\Database\DatabaseManager; use Kami\Cocktail\Models\CocktailFavorite; @@ -162,7 +163,9 @@ public function updateCocktail( $cocktail->description = $description; $cocktail->garnish = $garnish; $cocktail->source = $cocktailSource; - $cocktail->user_id = $userId; + if ($cocktail->user_id !== 1) { + $cocktail->user_id = $userId; + } $cocktail->glass_id = $glassId; $cocktail->save(); @@ -238,47 +241,13 @@ public function updateCocktail( */ public function getCocktailsByUserIngredients(int $userId) { - // Cocktails with possible ingredients - // SELECT c.id, c.name, count(*) as total FROM cocktails AS c - // INNER JOIN cocktail_ingredients AS ci ON ci.cocktail_id = c.id - // INNER JOIN ingredients AS i ON i.id = ci.ingredient_id - // WHERE ci.ingredient_id IN (SELECT ingredient_id FROM user_ingredients WHERE user_id = 2) - // GROUP BY c.id, c.name - // HAVING total <= (SELECT COUNT(*) FROM user_ingredients WHERE user_id = 2) - // ORDER BY total DESC - // LIMIT 10; - - // $cocktailIds = $this->db->table('cocktails AS c') - // ->select(['c.id']) - // ->join('cocktail_ingredients AS ci', 'ci.cocktail_id', '=', 'c.id') - // ->join('ingredients AS i', 'i.id', '=', 'ci.ingredient_id') - // ->whereIn('ci.ingredient_id', function ($query) use ($userId) { - // $query->select('ingredient_id') - // ->from('user_ingredients') - // ->where('user_id', $userId); - // }) - // ->groupBy('c.id', 'c.name') - // ->havingRaw('COUNT(*) <= 1') - // ->pluck('id'); - // return Cocktail::find($cocktailIds); - - // Cocktails strictly available // https://stackoverflow.com/questions/19930070/mysql-query-to-select-all-except-something - // SELECT c.* - // FROM cocktails c - // JOIN cocktail_ingredients ci ON ci.cocktail_id = c.id - // JOIN ingredients i ON i.id = ci.ingredient_id - // GROUP - // BY c.id - // HAVING SUM(CASE WHEN i.id IN (SELECT ingredient_id FROM user_ingredients WHERE user_id = 2) THEN 1 ELSE 0 END) = COUNT(*); - $cocktailIds = $this->db->table('cocktails AS c') ->select('c.id') ->join('cocktail_ingredients AS ci', 'ci.cocktail_id', '=', 'c.id') - ->join('ingredients AS i', 'i.id', '=', 'ci.ingredient_id') ->where('ci.optional', false) ->groupBy('c.id') - ->havingRaw('SUM(CASE WHEN i.id IN (SELECT ingredient_id FROM user_ingredients WHERE user_id = ?) THEN 1 ELSE 0 END) = COUNT(*)', [$userId]) + ->havingRaw('SUM(CASE WHEN ci.ingredient_id IN (SELECT ingredient_id FROM user_ingredients WHERE user_id = ?) THEN 1 ELSE 0 END) = COUNT(*)', [$userId]) ->pluck('id'); return Cocktail::find($cocktailIds); diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php index 3dfbaece..84aedf8a 100644 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@ -5,6 +5,7 @@ use Illuminate\Support\Str; use Kami\Cocktail\Models\Image; +use Illuminate\Http\UploadedFile; use Kami\Cocktail\Exceptions\ImageUploadException; class ImageService @@ -43,4 +44,17 @@ public function uploadAndSaveImages(array $requestImages): array return $images; } + + public function updateImage(int $imageId, ?UploadedFile $file = null, ?string $copyright = null): Image + { + $image = Image::findOrFail($imageId); + + if ($copyright) { + $image->copyright = $copyright; + } + + $image->save(); + + return $image; + } } diff --git a/app/Services/IngredientService.php b/app/Services/IngredientService.php index 3047d268..d28c098e 100644 --- a/app/Services/IngredientService.php +++ b/app/Services/IngredientService.php @@ -61,6 +61,11 @@ public function createIngredient( } } + // Refresh model for response + $ingredient->refresh(); + // Upsert scout index + $ingredient->save(); + return $ingredient; } @@ -117,6 +122,11 @@ public function updateIngredient( } } + // Refresh model for response + $ingredient->refresh(); + // Upsert scout index + $ingredient->save(); + return $ingredient; } } diff --git a/config/bar-assistant.php b/config/bar-assistant.php index 3ec9e905..e6c800bd 100644 --- a/config/bar-assistant.php +++ b/config/bar-assistant.php @@ -11,7 +11,7 @@ | */ - 'version' => 'v0.3.0', + 'version' => 'v0.4.0', /* |-------------------------------------------------------------------------- diff --git a/docs/spec.yml b/docs/spec.yml new file mode 100644 index 00000000..9568bf53 --- /dev/null +++ b/docs/spec.yml @@ -0,0 +1,595 @@ +openapi: '3.0.2' +info: + title: 'Bar Assistant API' + version: '1.0.0' + description: |- + Bar assistant is a self hosted application for managing your home bar. It allows you to add ingredients and create custom cocktail recipes. +servers: + - url: https://127.0.0.1:8000/api +tags: + - name: ingredients + description: Operations related to ingredients + - name: cocktails + description: Operations related to cocktails + - name: glasses + description: Operations related to glasses + - name: health + description: Operations related to server +security: + - user_token: [] +paths: + /version: + get: + tags: + - health + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Version' + /cocktails: + parameters: + - in: query + name: user_id + required: false + schema: + type: integer + example: 1 + description: 'Show only cocktails made by a specifc user' + get: + tags: + - cocktails + summary: 'Show a paginated list of cocktails' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Cocktail' + post: + tags: + - cocktails + summary: 'Create a new cocktail' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Cocktail' + /cocktails/{id}: + parameters: + - name: id + in: path + required: true + description: Database id of the cocktail + schema: + type: integer + get: + tags: + - cocktails + summary: 'Details of a specific cocktail' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Cocktail' + put: + tags: + - cocktails + summary: 'Update a specific cocktail' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Cocktail' + delete: + tags: + - cocktails + summary: 'Delete a specific cocktail' + responses: + '200': + description: OK + /cocktails/random: + get: + tags: + - cocktails + summary: 'Get a random cocktail' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Cocktail' + /cocktails/user-shelf: + get: + tags: + - cocktails + summary: 'Get a list of cocktails that currently authorized user can make' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Cocktail' + /ingredients: + get: + tags: + - ingredients + summary: 'Get a list of ingredients' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Ingredient' + post: + tags: + - ingredients + summary: 'Create a new ingredient' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IngredientRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Ingredient' + /ingredients/{id}: + parameters: + - name: id + in: path + required: true + description: Database id of the ingredient + schema: + type: integer + get: + tags: + - ingredients + summary: 'Details of a specific ingredient' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Ingredient' + put: + tags: + - ingredients + summary: 'Update an existing ingredient' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IngredientRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Ingredient' + delete: + tags: + - ingredients + summary: 'Delete a specific ingredient' + responses: + '200': + description: OK + /glasses: + get: + tags: + - glasses + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Glass' + /glasses/{id}: + get: + tags: + - glasses + parameters: + - in: path + name: id + schema: + type: integer + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Glass' +components: + securitySchemes: + user_token: + type: http + scheme: bearer + schemas: + Glass: + type: object + properties: + id: + type: integer + example: 1 + readOnly: true + name: + type: string + example: 'Cocktail glass' + description: + type: string + nullable: true + example: 'Description of glass' + IngredientRequest: + type: object + properties: + name: + type: string + example: 'Jack Daniels' + strength: + type: number + format: float + example: 40.0 + description: + type: string + example: 'A type of whiskey' + nullable: true + origin: + type: string + example: 'North America' + nullable: true + images: + type: array + example: [1, 2, 3] + items: + type: integer + ingredient_category_id: + type: integer + example: 1 + color: + type: string + format: hex + example: '#ffffff' + nullable: true + parent_ingredient_id: + type: integer + example: 1 + nullable: true + Ingredient: + type: object + properties: + id: + type: integer + example: 1 + readOnly: true + slug: + type: string + example: 'jack-daniels' + readOnly: true + name: + type: string + example: 'Jack Daniels' + strength: + type: number + format: float + example: 40.0 + description: + type: string + example: 'A type of whiskey' + origin: + type: string + example: 'North America' + main_image_id: + type: integer + example: 1 + nullable: true + images: + type: array + example: [1, 2, 3] + items: + type: integer + ingredient_category_id: + type: integer + example: 1 + color: + type: string + format: hex + example: '#ffffff' + nullable: true + category: + $ref: '#/components/schemas/IngredientCategory' + cocktails_count: + type: integer + example: 12 + varieties: + type: array + items: + type: object + properties: + id: + type: integer + example: 1 + slug: + type: string + example: 'ingredient-1' + name: + type: string + example: 'Ingredient 1' + cocktails: + type: array + items: + type: object + properties: + id: + type: integer + example: 1 + slug: + type: string + example: 'cocktail-1' + name: + type: string + example: 'Cocktail 1' + IngredientCategory: + type: object + properties: + id: + type: integer + example: 1 + readOnly: true + name: + type: string + example: Spirits + description: + type: string + nullable: true + example: 'A category of base spirits' + Version: + type: object + properties: + name: + type: string + example: 'Bar Assistant' + version: + type: string + example: 'v1.0.0' + meilisearch_host: + type: string + format: hostname + example: 'https://my-meilisearch-server.com' + meilisearch_version: + type: string + example: '0.29.0' + Image: + type: object + properties: + id: + type: integer + example: 1 + file_path: + type: string + example: 'ingredients/ingredient-image.png' + url: + type: string + example: 'http://localhost.com/app_images/ingredients/ingredient-image.png' + copyright: + type: string + example: 'Somewhere from the web' + last_modified: + type: string + example: '2022-11-17T09:52:48.000000Z' + User: + type: object + properties: + id: + type: integer + example: 1 + name: + type: string + example: 'Bar Tender' + email: + type: string + example: 'bar@tender.com' + search_host: + type: string + example: 'http://meilisearch-server.com' + search_api_key: + type: string + example: MEILI_API_KEY + favorite_cocktails: + type: array + example: [1, 2] + items: + type: integer + shelf_ingredients: + type: array + example: [1, 2] + items: + type: integer + shopping_lists: + type: array + example: [1, 2] + items: + type: integer + CocktailIngredientSubstitute: + type: object + properties: + id: + type: integer + example: 1 + slug: + type: string + example: 'ingredient-1' + name: + type: string + example: 'Ingredient 1' + CocktailIngredient: + type: object + properties: + id: + type: integer + example: 1 + sort: + type: integer + example: 0 + amount: + type: number + format: float + example: 30.0 + units: + type: string + example: ml + optional: + type: boolean + example: false + ingredient_id: + type: integer + example: 1 + name: + type: string + example: 'Ingredient name' + ingredient_slug: + type: string + example: 'ingredient-name' + substitutes: + type: array + items: + $ref: '#/components/schemas/CocktailIngredientSubstitute' + Cocktail: + type: object + properties: + id: + type: integer + example: 1 + readOnly: true + slug: + type: string + example: 'cocktail-name' + readOnly: true + name: + type: string + example: 'Cocktail name' + instructions: + type: string + nullable: true + example: |- + 1. Step + 2. Step + garnish: + type: string + nullable: true + example: Lemon wheel + description: + type: string + nullable: true + example: 'A short cocktail description' + source: + type: string + nullable: true + example: http://wikipedia.org + main_image_id: + type: integer + example: 1 + nullable: true + images: + type: array + items: + $ref: '#/components/schemas/Image' + tags: + type: array + example: ['Gin', 'IBA Official'] + items: + type: string + user_id: + type: integer + example: 1 + glass: + $ref: '#/components/schemas/Glass' + short_ingredients: + type: array + example: ['Gin', 'Tonic', 'Lemon Juice'] + items: + type: string + ingredients: + type: array + items: + $ref: '#/components/schemas/CocktailIngredient' + ErrorResponse: + type: object + properties: + type: + type: string + example: 'api_error' + message: + type: string + example: 'This is an detailed error message' + SuccessActionResponse: + type: object + properties: + success: + type: boolean + example: true diff --git a/resources/data/popular_cocktails.yml b/resources/data/popular_cocktails.yml new file mode 100644 index 00000000..85ff81ed --- /dev/null +++ b/resources/data/popular_cocktails.yml @@ -0,0 +1,610 @@ +- + name: 'Japanese cocktail' + description: |- + The Japanese cocktail is notorious for a handful of things: it's a very old cocktail, published in Jerry Thomas’s landmark 1862 book How to Mix Drinks; it was the first mixed drink to be named something other than “Whiskey Cocktail”, “Brandy Cocktail”, or some other similarly obvious epithet; it might be the only cocktail Thomas invented himself; and finally, it was the first cocktail to feature more than a dash of a fancy sweetener, orgeat. + instructions: |- + 1. Combine all ingredients in a mixing glass with ice and stir + 2. Strain into a coupe, serve up + garnish: 'Lemon peel' + source: null + image_copyright: 'The Drink Blog' + glass: Coupe + tags: + - Brandy + ingredients: + - + amount: 60 + units: ml + name: Brandy + optional: false + - + amount: 15 + units: ml + name: Orgeat Syrup + optional: false + - + amount: 2 + units: dashes + name: Angostura aromatic bitters + optional: false +- + name: 'Porn Star Martini' + description: |- + This easy passion fruit cocktail is bursting with zingy flavours and is perfect for celebrating with friends. + instructions: |- + 1. Scoop the seeds from one of the passion fruits into the glass of a cocktail shaker + 2. Add the vodka, passoa, lime juice and sugar syrup. + 3. Add a handful of ice and shake well + 4. Strain into a martini glass + 5. Serve with shot of chilled Champagne on the side + garnish: 'Half a passion fruit on top' + source: 'Douglas Ankrah, The Townhouse | London' + image_copyright: 'Punch / Jamie Lau' + glass: Coupe + tags: + - Vodka + ingredients: + - + amount: 60 + units: ml + name: Vanilla Vodka + optional: false + substitutes: [Vodka] + - + amount: 15 + units: ml + name: Passoã + optional: false + - + amount: 15 + units: ml + name: Simple syrup + optional: false + - + amount: 15 + units: ml + name: Lime juice + optional: false + - + amount: 60 + units: ml + name: Champagne + optional: true + substitutes: [Prosecco] +- + name: 'Amaretto Sour' + description: |- + Amaretto is an Italian liqueur that’s typically flavored with almonds or apricot stones. Its distinctive flavor can be incorporated into numerous cocktails, but it’s best known for the Amaretto Sour, a drink that tends to get a bad rap. That’s because, too often, the cocktail is overly sweet and relies on premade sour mix. + instructions: |- + 1. Combine all ingredients and, if using an egg white, dry shake + 2. Add ice and shake for 10 sec + 3. Strain into a coupe, serve up + garnish: 'Spray aromatic bitters over foaming cocktail from atomiser and then garnish with lemon & cherry sail (lemon slice & Luxardo Maraschino cherry on stick)' + source: '1974' + image_copyright: 'The Spruce Eats' + glass: Lowball + tags: + - Sour + ingredients: + - + amount: 60 + units: ml + name: Amaretto + optional: false + - + amount: 30 + units: ml + name: Lemon juice + optional: false + - + amount: 1 + units: dash + name: Angostura aromatic bitters + optional: false + - + amount: 15 + units: ml + name: Egg white + optional: true +- + name: 'Cantaritos' + description: |- + Cantaritos are Mexican tequila cocktails served in clay cups! Similar to the Paloma, this drink stars grapefruit soda and citrus. + instructions: |- + 1. If using the traditional clay cup for serving, soak it in cold water for 10 minutes before using. Otherwise, use a highball glass. + 2. Combine the tequila, orange juice, lemon juice and lime juice in the glass with a pinch of salt. + 3. Fill the glass with ice and top with grapefruit soda. + garnish: 'Citrus wedges' + source: 'Mexico' + image_copyright: 'A Couple Cooks' + glass: Highball + tags: + - Tequila + ingredients: + - + amount: 60 + units: ml + name: Tequila reposado + optional: false + - + amount: 45 + units: ml + name: Orange juice + optional: false + - + amount: 15 + units: ml + name: Lime juice + optional: false + - + amount: 15 + units: ml + name: Lemon juice + optional: false + - + amount: 90 + units: ml + name: Grapefruit juice + optional: false + - + amount: 2 + units: pinch + name: Salt + optional: true +- + name: 'White Negroni' + description: |- + The White Negroni is a fabulous use of the gentian's powers, and a downright brilliant twist on a bonafide classic. All of the flavor components of a classic Negroni are present, but only the gin remains verbatim. + instructions: |- + 1. Combine all ingredients with ice and stir + 2. Strain into a lowball glass + garnish: 'Lemon peel' + source: 'https://tuxedono2.com/white-negroni-cocktail-recipe' + image_copyright: 'A Couple Cooks' + glass: Lowball + tags: + - Gin + ingredients: + - + amount: 30 + units: ml + name: Gin + optional: false + - + amount: 30 + units: ml + name: Suze + optional: false + substitutes: [Salers] + - + amount: 30 + units: ml + name: Lillet Blanc + optional: false +- + name: 'B-52' + description: |- + The origins of the B-52 are not well documented, but one claim is that the B-52 was invented by Peter Fich, a head bartender at the Banff Springs Hotel in Alberta, Canada. Fich named all of his new drinks after favorite bands, albums, and songs, and he supposedly named the drink after the band of the same name, not directly after the US B-52 Stratofortress bomber after which the band was named. + instructions: |- + Refrigerate ingredients then layer in chilled glass by carefully pouring in the ingredient order. + garnish: null + source: 'https://en.wikipedia.org/wiki/B-52_(cocktail)' + image_copyright: Alchetron + glass: Shot + tags: + - Shot + ingredients: + - + amount: 15 + units: ml + name: Kahlua coffee liqueur + optional: false + - + amount: 15 + units: ml + name: Baileys Irish Cream + optional: false + - + amount: 15 + units: ml + name: Grand Marnier + optional: false +- + name: 'Bacardi Cocktail' + description: |- + The Bacardí Cocktail was originally the same as the Daiquiri, containing rum, lime juice, and sugar; The Grenadine version of the Bacardí Cocktail originated in the US, while the original non-red Bacardí company recipe originated from Cuba. + On April 28, 1936 the New York Supreme Court ruled that the drink must contain Bacardí rum in order to be called a Bacardí cocktail. + instructions: |- + Shake together with ice. Strain into glass and serve + garnish: 'Lime' + source: 'https://en.wikipedia.org/wiki/Bacardi_cocktail' + image_copyright: 'Liquor.com / Tim Nusog' + glass: Coupe + tags: + - Rum + ingredients: + - + amount: 60 + units: ml + name: White rum + optional: false + - + amount: 15 + units: ml + name: Lime juice + optional: false + - + amount: 7.5 + units: ml + name: Grenadine Syrup + optional: false + - + amount: 1 + units: barspoon + name: Simple syrup + optional: false +- + name: 'Bijou' + description: |- + This cocktail was invented by Harry Johnson, "the father of professional bartending", who called it bijou because it combined the colors of three jewels: gin for diamond, vermouth for ruby, and chartreuse for emerald. + instructions: |- + Stir in mixing glass with ice and strain + garnish: 'Maraschino cherry' + source: 'https://en.wikipedia.org/wiki/Bijou_(cocktail)' + image_copyright: 'A Couple Cooks' + glass: 'Nick and Nora' + tags: + - Gin + ingredients: + - + amount: 30 + units: ml + name: Gin + optional: false + - + amount: 30 + units: ml + name: Sweet vermouth + optional: false + - + amount: 30 + units: ml + name: Green Chartreuse + optional: false + - + amount: 2 + units: dashes + name: Orange bitters + optional: false +- + name: 'Gin & Tonic' + description: |- + Called a “G and T” or gin tonic in some countries, this refreshing drink is made in countries all around the world. The Gin and Tonic was invented in the 1850’s by British soldiers, who mixed gin with their tonic water as a way to drink quinine (which was thought to cure malaria). Tonic water of today no longer has quinine, but the drink stuck around! + instructions: |- + 1. Add lots of ice to a large cocktail or wine glass and stir to chill the glass. Drain any melted water. + 2. Pour in the gin. Add the garnishes. Pour the tonic water onto a bar spoon into the glass (to increase the bubbles). Stir once and serve. + garnish: 'Any of the following to spice your cocktail: Lime, lemon, cucumber, mint, orange peel, juniper berries, blood orange slice, rosemary' + source: 'https://www.acouplecooks.com/best-gin-and-tonic/' + image_copyright: 'A Couple Cooks' + glass: 'Wine' + tags: + - Gin + ingredients: + - + amount: 60 + units: ml + name: Gin + optional: false + - + amount: 120 + units: ml + name: Tonic + optional: false +- + name: 'Gin Gimlet' + description: |- + The word "gimlet" used in this sense is first attested in 1928. The most obvious derivation is from the tool for drilling small holes, a word also used figuratively to describe something as sharp or piercing. Thus, the cocktail may have been named for its "penetrating" effects on the drinker. + instructions: |- + 1. Add gin, lime juice, and syrup to a cocktail shaker. Fill with ice and shake until cold. + 2. Strain into glass and top with a splash of soda water, if desired. + garnish: 'Garnish with a lime wheel' + source: 'https://www.acouplecooks.com/gin-gimlet-cocktail/' + image_copyright: 'A Couple Cooks' + glass: 'Coupe' + tags: + - Gin + ingredients: + - + amount: 60 + units: ml + name: Gin + optional: false + - + amount: 15 + units: ml + name: Lime juice + optional: false + - + amount: 15 + units: ml + name: Simple syrup + optional: false + - + amount: 1 + units: splash + name: Club soda + optional: true +- + name: 'Sangria' + description: |- + A punch, sangria traditionally consists of red wine and chopped fruit, often with other ingredients or spirits. + + Sangria is very popular among foreign tourists in Spain even if locals do not consume the beverage that much. It is commonly served in bars, restaurants, and chiringuitos and at festivities throughout Portugal and Spain. + instructions: |- + 1. Chop the orange (leaving the peel on) and apple into bite-sized chunks, then add them to the bottom of a pitcher. Sprinkle them with sugar and stir. Let them stand for 20 minutes at room temperature. + 2. After 20 minutes, pour in the red wine, brandy, orange liqueur, and lemon rounds. Stir and refrigerate 1 to 4 hours. (Don’t go beyond 4 hours or the fruit texture starts to degrade.) + 3. Pour the sangria into ice filled glasses and top with a splash of sparkling water (if desired). Add fruit to each glass, preferably on long skewers for easy snacking. + garnish: 'Drop fruit chunks into a glass' + source: 'https://www.acouplecooks.com/gin-gimlet-cocktail/' + image_copyright: 'A Couple Cooks' + glass: 'Wine' + tags: + - Wine + ingredients: + - + amount: 1 + units: bottle + name: Red Wine + optional: false + - + amount: 1 + units: whole + name: Orange + optional: false + - + amount: 1 + units: whole + name: Apple + optional: false + - + amount: 3 + units: tablespoons + name: Sugar + optional: false + - + amount: 10 + units: ml + name: Brandy + optional: false + - + amount: 10 + units: ml + name: Cointreau + optional: false + - + amount: 1 + units: whole + name: Lemon + optional: false +- + name: '20th Century' + description: |- + The 20th Century is a cocktail created in 1937 by a British bartender named C.A. Tuck, and named in honor of the celebrated 20th Century Limited train which ran between New York City and Chicago from 1902 until 1967. + instructions: |- + 1. Combine all ingredients with ice and shake + 2. Strain into a coupe, serve up + garnish: 'Lemon twist' + source: 'https://en.wikipedia.org/wiki/20th_Century_(cocktail)' + image_copyright: 'Imbibe Magazine' + glass: 'Nick and Nora' + tags: + - Gin + ingredients: + - + amount: 45 + units: ml + name: Gin + optional: false + - + amount: 22.5 + units: ml + name: White Crème de Cacao + optional: false + - + amount: 22.5 + units: ml + name: Lillet Blanc + optional: false + - + amount: 15 + units: ml + name: Lemon juice + optional: false +- + name: 'Alaska' + description: |- + One of the great Chartreuse cocktails and a fundamental three-ingredient recipe + instructions: |- + 1. Combine all ingredients with ice in a mixing glass and stir at length, until the sides of the glass are frosty + 2. Strain into a cocktail glass and serve up + garnish: null + source: 'https://tuxedono2.com/alaska-cocktail-recipe' + image_copyright: Punch + glass: 'Nick and Nora' + tags: + - Gin + ingredients: + - + amount: 45 + units: ml + name: Gin + optional: false + - + amount: 15 + units: ml + name: Yellow Chartreuse + optional: false + - + amount: 1 + units: dash + name: Orange bitters + optional: false +- + name: 'Airmail' + description: |- + An endlessly drinkable sparkler of debated origin + instructions: |- + 1. Combine all ingredients except for the champagne in a mixer and shake for ten seconds + 2. Strain into a flute and top with champagne + garnish: null + source: 'https://en.wikipedia.org/wiki/Airmail_(cocktail)' + image_copyright: 'Imbibe Magazine' + glass: 'Cocktail' + tags: + - Rum + ingredients: + - + amount: 30 + units: ml + name: White Rum + optional: false + - + amount: 30 + units: ml + name: Champagne + optional: false + - + amount: 15 + units: ml + name: Lime juice + optional: false + - + amount: 15 + units: ml + name: Honey syrup + optional: false + substitutes: ['Simple syrup'] +- + name: 'Comte de Sureau' + description: null + instructions: 'Stir with ice and strain into a chilled rocks glass over ice.' + garnish: 'Garnish with orange and lemon twists.' + source: 'https://www.diffordsguide.com/cocktails/recipe/7257/comte-de-sureau' + image_copyright: "Eric's Cocktail Guide" + tags: + - Gin + glass: Lowball + ingredients: + - + amount: 45 + units: ml + name: Gin + optional: false + - + amount: 25 + units: ml + name: 'Elderflower Cordial' + optional: false + - + amount: 7.5 + units: ml + name: Campari + optional: false +- + name: Adonis + description: 'The cocktail was created in honor of the 1884 musical Adonis after the show reached the milestone of more than 500 shows on Broadway.' + instructions: 'Stir all ingredients with ice and strain into chilled glass.' + garnish: 'Orange zest and peel' + source: 'https://en.wikipedia.org/wiki/Adonis_(cocktail)' + image_copyright: 'Liquor.com / Tim Nusog' + tags: + - Wine + glass: Coupe + ingredients: + - + amount: 45 + units: ml + name: 'Dry Sherry' + optional: false + - + amount: 45 + units: ml + name: 'Sweet Vermouth' + optional: false + - + amount: 2 + units: dashes + name: 'Orange bitters' + optional: false +- + name: 'La Louisiane' + description: 'The La Louisiane cocktail is an improvement on the Sazerac! Absinthe, rye whiskey and vermouth make this spirit-forward cocktail a stunner.' + instructions: 'Add all ingredients to a cocktail mixing glass (or any other type of glass). Fill the mixing glass with 1 handful ice and stir continuously for 30 seconds until very cold.' + garnish: 'Garnish with a Luxardo cherry.' + source: 'https://www.acouplecooks.com/la-louisiane-cocktail/' + image_copyright: 'A couple cooks' + tags: + - Whiskey + glass: Cocktail + ingredients: + - + amount: 60 + units: ml + name: 'Rye whiskey' + optional: false + - + amount: 30 + units: ml + name: 'Sweet Vermouth' + optional: false + - + amount: 30 + units: ml + name: Bénédictine + optional: false + - + amount: 5 + units: ml + name: Absinthe + optional: false + - + amount: 3 + units: dashes + name: 'Peychauds Bitters' + optional: false +- + name: "Queen's Park Hotel Super Cocktail" + description: null + instructions: 'Shake all ingredients with ice and strain into chilled glass.' + garnish: 'Lime zest' + source: 'https://www.cocktailexplorer.co/cocktails/queens-park-hotel-super-cocktail/anders-erickson/' + image_copyright: null + tags: + - Rum + glass: Coupe + ingredients: + - + amount: 45 + units: ml + name: 'White Rum' + optional: false + - + amount: 15 + units: ml + name: 'Sweet Vermouth' + optional: false + - + amount: 15 + units: ml + name: 'Grenadine Syrup' + optional: false + - + amount: 15 + units: ml + name: 'Lime juice' + optional: false + - + amount: 2 + units: dashes + name: 'Angostura aromatic bitters' + optional: false diff --git a/resources/data/popular_cocktails_v0.3.0.yml b/resources/data/popular_cocktails_v0.3.0.yml deleted file mode 100644 index d0be6f6d..00000000 --- a/resources/data/popular_cocktails_v0.3.0.yml +++ /dev/null @@ -1,183 +0,0 @@ -- - name: 'Japanese cocktail' - description: |- - The Japanese cocktail is notorious for a handful of things: it's a very old cocktail, published in Jerry Thomas’s landmark 1862 book How to Mix Drinks; it was the first mixed drink to be named something other than “Whiskey Cocktail”, “Brandy Cocktail”, or some other similarly obvious epithet; it might be the only cocktail Thomas invented himself; and finally, it was the first cocktail to feature more than a dash of a fancy sweetener, orgeat. - instructions: |- - 1. Combine all ingredients in a mixing glass with ice and stir - 2. Strain into a coupe, serve up - garnish: 'Lemon peel' - source: null - image_copyright: 'The Drink Blog' - glass: Coupe - tags: - - Brandy - ingredients: - - - amount: 60 - units: ml - name: Brandy - optional: false - - - amount: 15 - units: ml - name: Orgeat Syrup - optional: false - - - amount: 2 - units: dashes - name: Angostura aromatic bitters - optional: false -- - name: 'Porn Star Martini' - description: |- - This easy passion fruit cocktail is bursting with zingy flavours and is perfect for celebrating with friends. - instructions: |- - 1. Scoop the seeds from one of the passion fruits into the glass of a cocktail shaker - 2. Add the vodka, passoa, lime juice and sugar syrup. - 3. Add a handful of ice and shake well - 4. Strain into a martini glass - 5. Serve with shot of chilled Champagne on the side - garnish: 'Half a passion fruit on top' - source: 'Douglas Ankrah, The Townhouse | London' - image_copyright: 'Punch / Jamie Lau' - glass: Coupe - tags: - - Vodka - ingredients: - - - amount: 60 - units: ml - name: Vanilla Vodka - optional: false - substitutes: [Vodka] - - - amount: 15 - units: ml - name: Passoã - optional: false - - - amount: 15 - units: ml - name: Simple syrup - optional: false - - - amount: 15 - units: ml - name: Lime juice - optional: false - - - amount: 60 - units: ml - name: Champagne - optional: true - substitutes: [Prosecco] -- - name: 'Amaretto Sour' - description: |- - Amaretto is an Italian liqueur that’s typically flavored with almonds or apricot stones. Its distinctive flavor can be incorporated into numerous cocktails, but it’s best known for the Amaretto Sour, a drink that tends to get a bad rap. That’s because, too often, the cocktail is overly sweet and relies on premade sour mix. - instructions: |- - 1. Combine all ingredients and, if using an egg white, dry shake - 2. Add ice and shake for 10 sec - 3. Strain into a coupe, serve up - garnish: 'Spray aromatic bitters over foaming cocktail from atomiser and then garnish with lemon & cherry sail (lemon slice & Luxardo Maraschino cherry on stick)' - source: '1974' - image_copyright: 'The Spruce Eats' - glass: Lowball - tags: - - Sour - ingredients: - - - amount: 60 - units: ml - name: Amaretto - optional: false - - - amount: 30 - units: ml - name: Lemon juice - optional: false - - - amount: 1 - units: dash - name: Angostura aromatic bitters - optional: false - - - amount: 15 - units: ml - name: Egg white - optional: true -- - name: 'Cantaritos' - description: |- - Cantaritos are Mexican tequila cocktails served in clay cups! Similar to the Paloma, this drink stars grapefruit soda and citrus. - instructions: |- - 1. If using the traditional clay cup for serving, soak it in cold water for 10 minutes before using. Otherwise, use a highball glass. - 2. Combine the tequila, orange juice, lemon juice and lime juice in the glass with a pinch of salt. - 3. Fill the glass with ice and top with grapefruit soda. - garnish: 'Citrus wedges' - source: 'Mexico' - image_copyright: 'A Couple Cooks' - glass: Highball - tags: - - Tequila - ingredients: - - - amount: 60 - units: ml - name: Tequila reposado - optional: false - - - amount: 45 - units: ml - name: Orange juice - optional: false - - - amount: 15 - units: ml - name: Lime juice - optional: false - - - amount: 15 - units: ml - name: Lemon juice - optional: false - - - amount: 90 - units: ml - name: Grapefruit juice - optional: false - - - amount: 2 - units: pinch - name: Salt - optional: true -- - name: 'White Negroni' - description: |- - The White Negroni is a fabulous use of the gentian's powers, and a downright brilliant twist on a bonafide classic. All of the flavor components of a classic Negroni are present, but only the gin remains verbatim. - instructions: |- - 1. Combine all ingredients with ice and stir - 2. Strain into a lowball glass - garnish: 'Lemon peel' - source: 'https://tuxedono2.com/white-negroni-cocktail-recipe' - image_copyright: 'A Couple Cooks' - glass: Lowball - tags: - - Gin - ingredients: - - - amount: 30 - units: ml - name: Gin - optional: false - - - amount: 30 - units: ml - name: Suze - optional: false - substitutes: [Salers] - - - amount: 30 - units: ml - name: Lillet Blanc - optional: false diff --git a/routes/api.php b/routes/api.php index 17606d28..160c6e8b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,7 @@ use Illuminate\Support\Facades\Route; use Kami\Cocktail\Http\Controllers\AuthController; use Kami\Cocktail\Http\Controllers\UserController; +use Kami\Cocktail\Http\Controllers\GlassController; use Kami\Cocktail\Http\Controllers\ImageController; use Kami\Cocktail\Http\Controllers\ShelfController; use Kami\Cocktail\Http\Controllers\HealthController; @@ -62,7 +63,7 @@ Route::get('/user-shelf', [CocktailController::class, 'userShelf'])->name('cocktails.user-shelf'); Route::get('/user-favorites', [CocktailController::class, 'userFavorites'])->name('cocktails.user-favorites'); Route::get('/{id}', [CocktailController::class, 'show'])->name('cocktails.show'); - Route::post('/{id}/favorite', [CocktailController::class, 'favorite'])->name('cocktails.favorite'); + Route::post('/{id}/toggle-favorite', [CocktailController::class, 'toggleFavorite'])->name('cocktails.favorite'); Route::post('/', [CocktailController::class, 'store'])->name('cocktails.store'); Route::delete('/{id}', [CocktailController::class, 'delete'])->name('cocktails.delete'); Route::put('/{id}', [CocktailController::class, 'update'])->name('cocktails.update'); @@ -71,6 +72,7 @@ Route::prefix('images')->group(function() { Route::get('/{id}', [ImageController::class, 'show']); Route::post('/', [ImageController::class, 'store']); + Route::post('/{id}', [ImageController::class, 'update']); Route::delete('/{id}', [ImageController::class, 'delete']); }); @@ -79,6 +81,11 @@ Route::delete('/batch', [ShoppingListController::class, 'batchDelete']); }); + Route::prefix('glasses')->group(function() { + Route::get('/', [GlassController::class, 'index']); + Route::get('/{id}', [GlassController::class, 'show']); + }); + }); Route::fallback(function() { diff --git a/storage/uploads/cocktails/20th-century.jpg b/storage/uploads/cocktails/20th-century.jpg new file mode 100644 index 00000000..a1ac42c3 Binary files /dev/null and b/storage/uploads/cocktails/20th-century.jpg differ diff --git a/storage/uploads/cocktails/adonis.jpg b/storage/uploads/cocktails/adonis.jpg new file mode 100644 index 00000000..10b041b2 Binary files /dev/null and b/storage/uploads/cocktails/adonis.jpg differ diff --git a/storage/uploads/cocktails/airmail.jpg b/storage/uploads/cocktails/airmail.jpg new file mode 100644 index 00000000..98bc0023 Binary files /dev/null and b/storage/uploads/cocktails/airmail.jpg differ diff --git a/storage/uploads/cocktails/alaska.jpg b/storage/uploads/cocktails/alaska.jpg new file mode 100644 index 00000000..912ae592 Binary files /dev/null and b/storage/uploads/cocktails/alaska.jpg differ diff --git a/storage/uploads/cocktails/b-52.jpg b/storage/uploads/cocktails/b-52.jpg new file mode 100644 index 00000000..3f54e215 Binary files /dev/null and b/storage/uploads/cocktails/b-52.jpg differ diff --git a/storage/uploads/cocktails/bacardi-cocktail.jpg b/storage/uploads/cocktails/bacardi-cocktail.jpg new file mode 100644 index 00000000..227b5e8b Binary files /dev/null and b/storage/uploads/cocktails/bacardi-cocktail.jpg differ diff --git a/storage/uploads/cocktails/bijou.jpg b/storage/uploads/cocktails/bijou.jpg new file mode 100644 index 00000000..ce5f115b Binary files /dev/null and b/storage/uploads/cocktails/bijou.jpg differ diff --git a/storage/uploads/cocktails/comte-de-sureau.jpg b/storage/uploads/cocktails/comte-de-sureau.jpg new file mode 100644 index 00000000..a6d770f6 Binary files /dev/null and b/storage/uploads/cocktails/comte-de-sureau.jpg differ diff --git a/storage/uploads/cocktails/gin-gimlet.jpg b/storage/uploads/cocktails/gin-gimlet.jpg new file mode 100644 index 00000000..0d7926c3 Binary files /dev/null and b/storage/uploads/cocktails/gin-gimlet.jpg differ diff --git a/storage/uploads/cocktails/gin-tonic.jpg b/storage/uploads/cocktails/gin-tonic.jpg new file mode 100644 index 00000000..c2743c0d Binary files /dev/null and b/storage/uploads/cocktails/gin-tonic.jpg differ diff --git a/storage/uploads/cocktails/la-louisiane.jpg b/storage/uploads/cocktails/la-louisiane.jpg new file mode 100644 index 00000000..5b56d7c3 Binary files /dev/null and b/storage/uploads/cocktails/la-louisiane.jpg differ diff --git a/storage/uploads/cocktails/no-image.jpg b/storage/uploads/cocktails/no-image.jpg deleted file mode 100644 index d78d1934..00000000 Binary files a/storage/uploads/cocktails/no-image.jpg and /dev/null differ diff --git a/storage/uploads/cocktails/queens-park-hotel-super-cocktail.jpg b/storage/uploads/cocktails/queens-park-hotel-super-cocktail.jpg new file mode 100644 index 00000000..4b03749b Binary files /dev/null and b/storage/uploads/cocktails/queens-park-hotel-super-cocktail.jpg differ diff --git a/storage/uploads/cocktails/sangria.jpg b/storage/uploads/cocktails/sangria.jpg new file mode 100644 index 00000000..8a265a03 Binary files /dev/null and b/storage/uploads/cocktails/sangria.jpg differ diff --git a/storage/uploads/ingredients/baileys-irish-cream.png b/storage/uploads/ingredients/baileys-irish-cream.png new file mode 100644 index 00000000..835954fe Binary files /dev/null and b/storage/uploads/ingredients/baileys-irish-cream.png differ diff --git a/storage/uploads/ingredients/dry-curacao.png b/storage/uploads/ingredients/dry-curacao.png new file mode 100644 index 00000000..701ea9c7 Binary files /dev/null and b/storage/uploads/ingredients/dry-curacao.png differ diff --git a/storage/uploads/ingredients/dry-sherry.png b/storage/uploads/ingredients/dry-sherry.png new file mode 100644 index 00000000..0dea9f22 Binary files /dev/null and b/storage/uploads/ingredients/dry-sherry.png differ diff --git a/storage/uploads/ingredients/no-image.png b/storage/uploads/ingredients/no-image.png deleted file mode 100644 index fa976a5c..00000000 Binary files a/storage/uploads/ingredients/no-image.png and /dev/null differ diff --git a/storage/uploads/ingredients/overproof-rum.png b/storage/uploads/ingredients/overproof-rum.png new file mode 100644 index 00000000..d1cd6715 Binary files /dev/null and b/storage/uploads/ingredients/overproof-rum.png differ