Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions app/Enums/AromaProfile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace HopsWeb\Enums;

enum AromaProfile: string
{
case Citrusy = "aroma_citrusy";
case Fruity = "aroma_fruity";
case Floral = "aroma_floral";
case Herbal = "aroma_herbal";
case Spicy = "aroma_spicy";
case Resinous = "aroma_resinous";
case Sugarlike = "aroma_sugarlike";
case Miscellaneous = "aroma_misc";

public function label(): string
{
return match($this) {
self::Citrusy => "Citrusy",
self::Fruity => "Fruity",
self::Floral => "Floral",
self::Herbal => "Herbal",
self::Spicy => "Spicy",
self::Resinous => "Resinous",
self::Sugarlike => "Sweet/Sugarlike",
self::Miscellaneous => "Miscellaneous",
};
}

public function color(): string
{
return match($this) {
self::Citrusy => "orange-500",
self::Fruity => "red-500",
self::Floral => "pink-400",
self::Herbal => "green-500",
self::Spicy => "amber-700",
self::Resinous => "emerald-700",
self::Sugarlike => "yellow-400",
self::Miscellaneous => "slate-400",
};
}
}
33 changes: 33 additions & 0 deletions app/Http/Controllers/HopController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace HopsWeb\Http\Controllers;

use HopsWeb\Models\Hop;
use Illuminate\Http\Request;
use Illuminate\View\View;

class HopController extends Controller
{
public function index(Request $request): View
{
$hops = Hop::query()
->filter($request->all())
->orderBy("name")
->paginate(12)
->withQueryString();

return view("hops.index", [
"hops" => $hops,
"filters" => $request->all(),
]);
}

public function show(Hop $hop): View
{
return view("hops.show", [
"hop" => $hop,
]);
}
}
45 changes: 45 additions & 0 deletions app/Models/Hop.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace HopsWeb\Models;

use HopsWeb\Casts\RangeOrNumberCast;
use HopsWeb\Enums\AromaProfile;
use HopsWeb\Enums\Aromaticity;
use HopsWeb\Enums\Bitterness;
use HopsWeb\Enums\HopDescriptor;
Expand Down Expand Up @@ -121,5 +122,49 @@ class Hop extends Model
"lineage" => "array",
"aroma_descriptors" => "array",
"substitutes" => "array",
"bitterness" => Bitterness::class,
"aromaticity" => Aromaticity::class,
];

public function scopeFilter($query, array $filters)
{
foreach (self::RANGE_FIELDS as $field) {
if (isset($filters[$field . "_min"])) {
$query->where($field . "_max", ">=", $filters[$field . "_min"]);
}

if (isset($filters[$field . "_max"])) {
$query->where($field . "_min", "<=", $filters[$field . "_max"]);
}
}

foreach (AromaProfile::cases() as $profile) {
$flag = $profile->value;

if (isset($filters[$flag]) && $filters[$flag] === "1") {
$query->where($flag, 1);
}
}

if (isset($filters["bitterness"]) && $filters["bitterness"] !== "all") {
$query->where("bitterness", $filters["bitterness"]);
}

if (isset($filters["aromaticity"]) && $filters["aromaticity"] !== "all") {
$query->where("aromaticity", $filters["aromaticity"]);
}

return $query;
}

/**
* @return array<AromaProfile>
*/
public function getActiveAromas(): array
{
return array_filter(
AromaProfile::cases(),
fn(AromaProfile $profile) => (bool)$this->{$profile->value},
);
}
}
2 changes: 1 addition & 1 deletion app/ValueObjects/RangeOrNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function __construct(

public static function fromNumber(float $value): self
{
return new self(null, null, $value);
return new self($value, $value, $value);
}

public static function fromRange(float $min, float $max): self
Expand Down
1 change: 1 addition & 0 deletions resources/css/app.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@import 'tailwindcss';
@source "../views/**/*.blade.php";
@plugin "@tailwindcss/forms";
107 changes: 107 additions & 0 deletions resources/views/components/hops/filters.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
@props(['filters'])

<aside class="bg-white border-r border-gray-200 w-80 flex-shrink-0 hidden lg:block overflow-y-auto"
x-data="{
autoSubmit() {
if (this.$refs.filterForm.checkValidity()) {
this.$refs.filterForm.submit();
}
}
}">
<div class="p-6">
<div class="flex items-center justify-between mb-8">
<h2 class="text-lg font-bold text-gray-900">Filters</h2>
<a href="{{ route('hops.index') }}" class="text-sm text-green-600 hover:text-green-700 font-medium">Clear all</a>
</div>

<form action="{{ route('hops.index') }}" method="GET" x-ref="filterForm">
<div class="space-y-8">

<div>
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Aroma Profile</h3>
<div class="space-y-3">
@foreach(\HopsWeb\Enums\AromaProfile::cases() as $profile)
<label class="flex items-center group cursor-pointer">
<input type="checkbox" name="{{ $profile->value }}" value="1"
@change="autoSubmit()"
{{ ($filters[$profile->value] ?? '') == '1' ? 'checked' : '' }}
class="h-4 w-4 rounded border-gray-300 text-green-600 focus:ring-green-500">
<span class="ml-3 text-sm text-gray-600 group-hover:text-gray-900 transition-colors">{{ $profile->label() }}</span>
</label>
@endforeach
</div>
</div>


<div>
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Biochemical Properties</h3>
<div class="space-y-6">
@foreach([
'alpha_acid' => 'Alpha Acid (%)',
'beta_acid' => 'Beta Acid (%)',
'cohumulone' => 'Cohumulone (%)',
'total_oil' => 'Total Oil (ml/100g)'
] as $key => $label)
<div class="space-y-2" x-data="{
min: '{{ $filters[$key . '_min'] ?? '' }}',
max: '{{ $filters[$key . '_max'] ?? '' }}'
}">
<span class="text-xs font-medium text-gray-500 uppercase">{{ $label }}</span>
<div class="flex items-center space-x-2">
<input type="number" name="{{ $key }}_min" x-model="min"
placeholder="Min" step="0.1" min="0" :max="max || 100"
@change.debounce.500ms="autoSubmit()"
class="w-full text-xs rounded-md border-gray-300 focus:border-green-500 focus:ring-green-500">
<span class="text-gray-400">-</span>
<input type="number" name="{{ $key }}_max" x-model="max"
placeholder="Max" step="0.1" :min="min || 0" max="100"
@change.debounce.500ms="autoSubmit()"
class="w-full text-xs rounded-md border-gray-300 focus:border-green-500 focus:ring-green-500">
</div>
</div>
@endforeach
</div>
</div>


<div>
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Characteristics</h3>
<div class="space-y-4">
<div class="space-y-2">
<label class="text-xs font-medium text-gray-500 uppercase">Bitterness</label>
<select name="bitterness" @change="autoSubmit()"
class="w-full text-sm rounded-md border-gray-300 focus:border-green-500 focus:ring-green-500">
<option value="all">Any</option>
@foreach(\HopsWeb\Enums\Bitterness::cases() as $case)
<option value="{{ $case->value }}" {{ ($filters['bitterness'] ?? '') == $case->value ? 'selected' : '' }}>
{{ ucfirst($case->value) }}
</option>
@endforeach
</select>
</div>
<div class="space-y-2">
<label class="text-xs font-medium text-gray-500 uppercase">Aromaticity</label>
<select name="aromaticity" @change="autoSubmit()"
class="w-full text-sm rounded-md border-gray-300 focus:border-green-500 focus:ring-green-500">
<option value="all">Any</option>
@foreach(\HopsWeb\Enums\Aromaticity::cases() as $case)
<option value="{{ $case->value }}" {{ ($filters['aromaticity'] ?? '') == $case->value ? 'selected' : '' }}>
{{ ucfirst($case->value) }}
</option>
@endforeach
</select>
</div>
</div>
</div>
</div>

<noscript>
<div class="mt-8">
<button type="submit" class="w-full bg-green-700 text-white rounded-md py-2 text-sm font-semibold hover:bg-green-800 transition">
Apply Filters
</button>
</div>
</noscript>
</form>
</div>
</aside>
23 changes: 23 additions & 0 deletions resources/views/components/hops/header.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<header class="bg-green-800 text-white shadow-md">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="bg-green-600 p-2 rounded-lg">
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</div>
<h1 class="text-2xl font-bold tracking-tight">Hop Variety Browser</h1>
</div>
<div>
@if (Route::has('login'))
<nav class="flex space-x-4">
@auth
<a href="{{ url('/dashboard') }}" class="text-green-100 hover:text-white transition">Dashboard</a>
@else
<a href="{{ route('login') }}" class="text-green-100 hover:text-white transition">Log in</a>
@endauth
</nav>
@endif
</div>
</div>
</header>
72 changes: 72 additions & 0 deletions resources/views/components/hops/hop-card.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@props(['hop'])


<a href="{{ route('hops.show', $hop) }}" class="group block bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow">
<div class="p-6">
<div class="flex justify-between items-start mb-4">
<div>
<h3 class="text-lg font-bold text-gray-900 group-hover:text-green-700 transition-colors">
{{ $hop->name }}
</h3>
<p class="text-sm text-gray-500 leading-tight">
{{ $hop->country ?? 'Unknown origin' }}
</p>
</div>

</div>

<div class="grid grid-cols-2 gap-4 text-sm mt-4">
<div class="bg-gray-50 p-3 rounded-lg">
<span class="text-gray-500 block text-xs uppercase tracking-wider font-semibold mb-1">Alpha Acid</span>
<span class="text-gray-900 font-bold">
@if($hop->alpha_acid?->min === $hop->alpha_acid?->max)
{{ $hop->alpha_acid?->min }}%
@else
{{ $hop->alpha_acid?->min ?? 'N/A' }} - {{ $hop->alpha_acid?->max ?? 'N/A' }}%
@endif
</span>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<span class="text-gray-500 block text-xs uppercase tracking-wider font-semibold mb-1">Beta Acid</span>
<span class="text-gray-900 font-bold">
@if($hop->beta_acid?->min === $hop->beta_acid?->max)
{{ $hop->beta_acid?->min }}%
@else
{{ $hop->beta_acid?->min ?? 'N/A' }} - {{ $hop->beta_acid?->max ?? 'N/A' }}%
@endif
</span>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<span class="text-gray-500 block text-xs uppercase tracking-wider font-semibold mb-1">Cohumulone</span>
<span class="text-gray-900 font-bold">
@if($hop->cohumulone?->min === $hop->cohumulone?->max)
{{ $hop->cohumulone?->min }}%
@else
{{ $hop->cohumulone?->min ?? 'N/A' }} - {{ $hop->cohumulone?->max ?? 'N/A' }}%
@endif
</span>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<span class="text-gray-500 block text-xs uppercase tracking-wider font-semibold mb-1">Total Oil</span>
<span class="text-gray-900 font-bold">
@if($hop->total_oil?->min === $hop->total_oil?->max)
{{ $hop->total_oil?->min }}%
@else
{{ $hop->total_oil?->min ?? 'N/A' }} - {{ $hop->total_oil?->max ?? 'N/A' }}%
@endif
</span>
</div>
</div>

<div class="mt-6 flex items-center justify-between">

<div class="text-sm font-semibold text-green-700 group-hover:text-green-800 flex items-center transition-colors">
Full Details
<svg class="w-4 h-4 ml-1 transform group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</a>

25 changes: 25 additions & 0 deletions resources/views/components/hops/layout.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full bg-gray-50">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title ?? 'Hop Variety Browser' }} - {{ config('app.name', 'Laravel') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.ts'])
</head>
<body class="h-full text-gray-900 font-sans antialiased">
<div class="min-h-full flex flex-col">
<x-hops.header />
<x-hops.navigation />

<main class="flex-grow">
{{ $slot }}
</main>

<footer class="bg-white border-t border-gray-200 py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-gray-500 text-sm">
&copy; {{ date('Y') }} {{ config('app.name') }}. All rights reserved.
</div>
</footer>
</div>
</body>
</html>
15 changes: 15 additions & 0 deletions resources/views/components/hops/navigation.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<nav class="bg-white border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex h-12 items-center">
<div class="flex space-x-8">
<a href="{{ route('hops.index') }}"
class="{{ request()->routeIs('hops.index') ? 'border-green-500 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-green-300' }} inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
All Varieties
</a>
<a href="#" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
About Hops
</a>
</div>
</div>
</div>
</nav>
Loading