Add Goal controller, routes, and resources

This commit is contained in:
Christopher C. Wells 2021-02-13 06:07:31 -08:00
parent 74fe2be70e
commit eee1cfaaf1
8 changed files with 276 additions and 5 deletions

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers;
use App\Models\Goal;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class GoalController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): View
{
return view('goals.index');
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Contracts\View\View
*/
public function create(): View
{
return $this->edit(new Goal());
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): RedirectResponse
{
return $this->update($request, new Goal());
}
/**
* Display the specified resource.
*/
public function show(Goal $goal): View
{
return view('goals.show')->with('goal', $goal);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Goal $goal): View
{
return view('goals.edit')
->with('goal', $goal)
->with('attributeOptions', Goal::getAttributeOptions());
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Goal $goal): RedirectResponse
{
$attributes = $request->validate([
'from' => ['nullable', 'date'],
'to' => ['nullable', 'date'],
'attribute' => ['required', 'string'],
'goal' => ['required', 'numeric'],
]);
$goal->fill(array_filter($attributes))
->user()->associate(Auth::user());
$goal->save();
session()->flash('message', "Goal updated!");
return redirect()->route('goals.show', $goal);
}
/**
* Confirm removal of specified resource.
*/
public function delete(Goal $goal): View
{
return view('goals.delete')->with('goal', $goal);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Goal $goal): RedirectResponse
{
$goal->delete();
return redirect(route('goals.index'))
->with('message', "Goal deleted!");
}
}

View File

@ -2,9 +2,11 @@
namespace App\Models; namespace App\Models;
use App\Support\Nutrients;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
/** /**
* App\Models\Goal * App\Models\Goal
@ -60,4 +62,23 @@ class Goal extends Model
public function user(): BelongsTo { public function user(): BelongsTo {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
/**
* Get options for the "attribute" column.
*
* @return array
*/
public static function getAttributeOptions(): array {
$options = [];
foreach (Nutrients::$all as $nutrient) {
foreach (['daily', 'weekly', 'monthly', 'yearly'] as $frequency) {
$key = "{$nutrient['value']}_{$frequency}";
$options[$key] = [
'value' => $key,
'label' => Str::ucfirst("{$frequency} {$nutrient['value']}")
];
}
}
return $options;
}
} }

View File

@ -0,0 +1,28 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Delete {{ $goal->attribute }} Goal?
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<form method="POST" action="{{ route('goals.destroy', $goal) }}">
@method('delete')
@csrf
<div class="text-lg pb-3">
Are you sure what to delete your <span class="font-extrabold">{{ $goal->attribute }}</span> goal?
</div>
<x-inputs.button class="bg-red-800 hover:bg-red-700">
Yes, delete
</x-inputs.button>
<a class="ml-3 text-gray-500 hover:text-gray-700 hover:border-gray-300"
href="{{ route('goals.show', $goal) }}">No, do not delete</a>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,70 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ ($goal->exists ? 'Edit' : 'Add') }} Goal
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<form method="POST" action="{{ ($goal->exists ? route('goals.update', $goal) : route('goals.store')) }}">
@if ($goal->exists)@method('put')@endif
@csrf
<div class="flex flex-col space-y-4">
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
<!-- From -->
<div class="flex-auto">
<x-inputs.label for="from" value="From"/>
<x-inputs.input name="from"
type="date"
class="block w-full"
:value="old('from', $goal->from)" />
</div>
<!-- To -->
<div class="flex-auto">
<x-inputs.label for="to" value="To"/>
<x-inputs.input name="to"
type="date"
class="block w-full"
:value="old('to', $goal->to)" />
</div>
<!-- Attribute -->
<div class="flex-auto">
<x-inputs.label for="attribute" value="Attribute"/>
<x-inputs.select name="attribute"
class="block mt-1 w-full"
:options="$attributeOptions"
:selectedValue="old('attribute', $goal->attribute)">
<option value=""></option>
</x-inputs.select>
</div>
<!-- Goal -->
<div class="flex-auto">
<x-inputs.label for="goal" value="Goal"/>
<x-inputs.input name="goal"
type="number"
step="any"
class="block mt-1 w-full"
:value="old('goal', $goal->goal)"/>
</div>
</div>
</div>
<div class="flex items-center justify-end mt-4">
<x-inputs.button class="ml-3">
{{ ($goal->exists ? 'Save' : 'Add') }}
</x-inputs.button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,23 @@
<x-app-layout>
<x-slot name="header">
<div class="flex justify-between items-center">
<h2 class="font-semibold text-2xl text-gray-800 leading-tight">Goals</h2>
<a href="{{ route('goals.create') }}" class="inline-flex items-center rounded-md font-semibold text-white p-2 bg-green-500 tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-600 disabled:opacity-25 transition ease-in-out duration-150">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
Add Goal
</a>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
TODO: Goals index.
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,29 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight flex flex-auto">
TODO: GOAL NAME
<a class="ml-2 text-gray-500 hover:text-gray-700 hover:border-gray-300 text-sm"
href="{{ route('goals.edit', $goal) }}">
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" />
</svg>
</a>
<a class="h-6 w-6 text-red-500 hover:text-red-700 hover:border-red-300 float-right text-sm"
href="{{ route('goals.delete', $goal) }}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</a>
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
TODO: GOAL SHOW PAGE
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -62,6 +62,10 @@
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div> <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div> </div>
<div class="mt-3 space-y-1">
<x-dropdown-link :href="route('goals.index')">Goals</x-dropdown-link>
</div>
<div class="mt-3 space-y-1"> <div class="mt-3 space-y-1">
<!-- Authentication --> <!-- Authentication -->
<form method="POST" action="{{ route('logout') }}" x-data> <form method="POST" action="{{ route('logout') }}" x-data>

View File

@ -1,6 +1,7 @@
<?php <?php
use App\Http\Controllers\FoodController; use App\Http\Controllers\FoodController;
use App\Http\Controllers\GoalController;
use App\Http\Controllers\IngredientPickerController; use App\Http\Controllers\IngredientPickerController;
use App\Http\Controllers\JournalEntryController; use App\Http\Controllers\JournalEntryController;
use App\Http\Controllers\RecipeController; use App\Http\Controllers\RecipeController;
@ -27,8 +28,12 @@ Route::get('/', function (): RedirectResponse {
Route::resource('foods', FoodController::class)->middleware(['auth']); Route::resource('foods', FoodController::class)->middleware(['auth']);
Route::get('/foods/{food}/delete', [FoodController::class, 'delete'])->middleware(['auth'])->name('foods.delete'); Route::get('/foods/{food}/delete', [FoodController::class, 'delete'])->middleware(['auth'])->name('foods.delete');
// Recipes. // Goals.
Route::resource('recipes', RecipeController::class)->middleware(['auth']); Route::resource('goals', GoalController::class)->middleware(['auth']);
Route::get('/goals/{goal}/delete', [GoalController::class, 'delete'])->middleware(['auth'])->name('goals.delete');
// Ingredient picker.
Route::get('/ingredient-picker/search', [IngredientPickerController::class, 'search'])->middleware(['auth'])->name('ingredient-picker.search');
// Journal entries. // Journal entries.
Route::get('/journal-entries/create/from-nutrients', [JournalEntryController::class, 'createFromNutrients'])->middleware(['auth'])->name('journal-entries.create.from-nutrients'); Route::get('/journal-entries/create/from-nutrients', [JournalEntryController::class, 'createFromNutrients'])->middleware(['auth'])->name('journal-entries.create.from-nutrients');
@ -36,8 +41,7 @@ Route::post('/journal-entries/create/from-nutrients', [JournalEntryController::c
Route::resource('journal-entries', JournalEntryController::class)->middleware(['auth']); Route::resource('journal-entries', JournalEntryController::class)->middleware(['auth']);
Route::get('/journal-entries/{journalEntry}/delete', [JournalEntryController::class, 'delete'])->middleware(['auth'])->name('journal-entries.delete'); Route::get('/journal-entries/{journalEntry}/delete', [JournalEntryController::class, 'delete'])->middleware(['auth'])->name('journal-entries.delete');
// Custom. // Recipes.
// TODO: Change this to a custom JSON API endpoint. Route::resource('recipes', RecipeController::class)->middleware(['auth']);
Route::get('/ingredient-picker/search', [IngredientPickerController::class, 'search'])->middleware(['auth'])->name('ingredient-picker.search');
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';