refactor(Infinite Loading):

Modified Pomo dashboard to support mobile devices & Infinite loading

Added Request Validation for Pomo
This commit is contained in:
devoalda 2023-08-09 09:59:59 +08:00
parent 99d37aafb6
commit 49ad03f1cb
17 changed files with 182 additions and 113 deletions

View File

@ -21,7 +21,7 @@ class DashboardController extends Controller
->whereDate('due_end', '<=', strtotime('today midnight')) ->whereDate('due_end', '<=', strtotime('today midnight'))
->whereNull('completed_at') ->whereNull('completed_at')
->orderBy('due_end', 'asc') ->orderBy('due_end', 'asc')
->paginate(5); ->paginate(5, $columns = ['*'], $pageName = 'todos');
$todos->transform(function ($todo) { $todos->transform(function ($todo) {
return \App\Models\Todo::find($todo->id); return \App\Models\Todo::find($todo->id);

View File

@ -46,10 +46,10 @@ class PomoController extends Controller
// Convert due_start and end to unix timestamp and save // Convert due_start and end to unix timestamp and save
$pomo = new Pomo(); $pomo = new Pomo();
$pomo->todo_id = $request->todo_id; $pomo->todo_id = $request->safe()->todo_id;
$pomo->pomo_start = strtotime($request->pomo_start); $pomo->pomo_start = strtotime($request->safe()->pomo_start);
$pomo->pomo_end = strtotime($request->pomo_end); $pomo->pomo_end = strtotime($request->safe()->pomo_end);
$pomo->notes = $request->notes; $pomo->notes = $request->safe()->notes;
$pomo->save(); $pomo->save();
return redirect()->route('pomo.index') return redirect()->route('pomo.index')

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Http\Requests\Project\StoreProjectRequest; use App\Http\Requests\Project\StoreProjectRequest;
use App\Http\Requests\Project\UpdateProjectRequest; use App\Http\Requests\Project\UpdateProjectRequest;
use App\Models\Project; use App\Models\Project;
use App\Models\Todo;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
@ -26,9 +27,11 @@ class ProjectController extends Controller
$user = User::find(auth()->user()->id); $user = User::find(auth()->user()->id);
$projects = $user->projects()->paginate(4); $projects = $user->projects()->paginate(4);
// Aggregate all todos for all projects // Aggregate all todos for all projects
$todos = $projects->map(function ($project) {
return $project->todos; $todos = $user->todos()
})->flatten(); ->map(function ($todo) {
return Todo::find($todo->id);
});
if ($request->ajax()){ if ($request->ajax()){
$view = view('project.load-projects', compact('projects'))->render(); $view = view('project.load-projects', compact('projects'))->render();
@ -41,7 +44,9 @@ class ProjectController extends Controller
return view('project.index', [ return view('project.index', [
'projects' => $projects, 'projects' => $projects,
'todos' => $todos->whereNull('completed_at')->values(), 'todos' => $todos->whereNull('completed_at')->values(),
'completed' => $todos->whereNotNull('completed_at')->values(), 'completed' => $todos->whereNotNull('completed_at')
->whereBetween('completed_at', [strtotime('today midnight'), strtotime('today midnight + 1 day')])
->values(),
]); ]);
} }

View File

@ -8,10 +8,11 @@ use App\Models\{
Project, Project,
Todo Todo
}; };
use Carbon\Carbon;
class PomoTime extends Component class PomoTime extends Component
{ {
public int $ave_pomo_time = 0; public $ave_pomo_time = 0;
public function mount() public function mount()
{ {
@ -35,6 +36,9 @@ class PomoTime extends Component
$this->ave_pomo_time = $total_time / $total_pomos; $this->ave_pomo_time = $total_time / $total_pomos;
// Time in Hours and Minutes (H hours m minutes)
$this->ave_pomo_time = Carbon::createFromTimestamp($this->ave_pomo_time)->format('H \h m \m');
} }

View File

@ -3,11 +3,9 @@
namespace App\Http\Livewire\Pomo; namespace App\Http\Livewire\Pomo;
use App\Models\Pomo; use App\Models\Pomo;
use App\Models\Project;
use App\Models\Todo;
use App\Models\User; use App\Models\User;
use Livewire\Component;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component;
class Create extends Component class Create extends Component
{ {

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Livewire\Pomo;
use Livewire\Component;
class PomoDashboard extends Component
{
public function render()
{
return view('livewire.pomo.pomo-dashboard');
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Livewire\Pomo; namespace App\Http\Livewire\Pomo;
use Illuminate\Support\Facades\DB;
use Livewire\Component; use Livewire\Component;
use App\Models\{ use App\Models\{
Pomo, Pomo,
@ -12,26 +13,29 @@ use App\Models\{
class Pomos extends Component class Pomos extends Component
{ {
public $perPage = 10; public $perPage = 9;
public $listeners = [
'load-more' => 'loadMore',
];
public function loadMore() public function loadMore()
{ {
$this->perPage += 10; $this->perPage += 9;
} }
public function render() public function render()
{ {
$user = User::find(auth()->id()); $user = User::find(auth()->id());
$pomos = $user->pomos();
// Convert Pomos from Collection to class
$pomos = $pomos->map(function ($pomo) {
$pomo->todo = Todo::find($pomo->todo_id);
$pomo->project = Project::find($pomo->todo->project_id);
return $pomo;
});
$pomos = Pomo::whereHas('todo', function ($query) use ($user) {
$query->whereHas('project', function ($query) use ($user) {
$query->whereHas('user', function ($query) use ($user) {
$query->where('user_id', $user->id);
});
});
})->orderBy('pomo_start', 'desc')->paginate($this->perPage);
return view('livewire.pomo.pomos', [ return view('livewire.pomo.pomos', [
'pomos' => $pomos, 'pomos' => $pomos,

View File

@ -3,6 +3,7 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Carbon\Carbon;
class StorePomoRequest extends FormRequest class StorePomoRequest extends FormRequest
{ {
@ -23,9 +24,16 @@ class StorePomoRequest extends FormRequest
{ {
return [ return [
'todo_id' => 'required|exists:todos,id', 'todo_id' => 'required|exists:todos,id',
'pomo_start' => 'required|date', 'pomo_start' => 'required|date_format:Y-m-d\TH:i',
'pomo_end' => 'required|date', 'pomo_end' => 'required|date_format:Y-m-d\TH:i|after:pomo_start',
'notes' => 'nullable|string', 'notes' => 'nullable|string',
]; ];
} }
public function messages()
{
return [
'todo_id.required' => 'Please select a todo.',
];
}
} }

View File

@ -22,7 +22,9 @@ class UpdatePomoRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
// 'pomo_start' => 'required|date_format:Y-m-d\TH:i',
'pomo_end' => 'required|date_format:Y-m-d\TH:i|after:pomo_start',
'notes' => 'nullable|string',
]; ];
} }
} }

View File

@ -14,9 +14,9 @@ class DatabaseSeeder extends Seeder
public function run(): void public function run(): void
{ {
\App\Models\User::factory(3) \App\Models\User::factory(3)
->has(\App\Models\Project::factory()->count(3) ->has(\App\Models\Project::factory()->count(1)
->has(\App\Models\Todo::factory()->count(10) ->has(\App\Models\Todo::factory()->count(5)
->has(\App\Models\Pomo::factory()->count(4)) ->has(\App\Models\Pomo::factory()->count(20))
)) ))
->create(); ->create();
} }

View File

@ -1,19 +1,19 @@
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6">
<h3 class="text-xl font-semibold mb-4 text-blue-500 dark:text-blue-400"> <h3 class="text-xl font-semibold mb-4 text-blue-500 dark:text-blue-400">
<!-- SVG for Pomo Average Count --> <!-- SVG for Pomo Average Count -->
<svg class="inline-block h-6 w-6 text-blue-500 dark:text-blue-400" <svg class="inline-block h-6 w-6 text-blue-500 dark:text-blue-400"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor">
<path stroke-linecap="round" <path stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"/> d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg> </svg>
Average Pomos per Project Average Pomos per Project
</h3> </h3>
<p class="text-3xl font-bold text-gray-800 dark:text-gray-100"> <p class="text-3xl font-bold text-gray-800 dark:text-gray-100">
{{ $ave_pomo_count }} {{ $ave_pomo_count }}
</p> </p>
</div> </div>

View File

@ -11,7 +11,7 @@
stroke-width="2" stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"/> d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg> </svg>
Average Pomos time spent per Project Average time spent per Project
</h3> </h3>
<p class="text-3xl font-bold text-gray-800 dark:text-gray-100"> <p class="text-3xl font-bold text-gray-800 dark:text-gray-100">
{{ $ave_pomo_time }} {{ $ave_pomo_time }}

View File

@ -12,11 +12,13 @@
<div class="mb-4"> <div class="mb-4">
<label for="todo_id" class="block mb-2 font-semibold">Todo</label> <label for="todo_id" class="block mb-2 font-semibold">Todo</label>
<select class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-blue-500 rounded-lg w-full p-4 @error('todo_id') border-red-500 @enderror" <select class="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-700 focus:ring-2 focus:ring-blue-500 rounded-lg w-full p-4 @error('todo_id') border-red-500 @enderror"
name="todo_id" id="todo_id"> name="todo_id" id="todo_id" {{ $editing ? 'disabled' : '' }}>
<option selected value="{{ $editing ? $pomo->todo_id : old('todo_id') }}">{{ $editing ? $pomo->todo->title : 'Select a Todo' }}</option> <option selected value="{{ $editing ? $pomo->todo_id : old('todo_id') }}">{{ $editing ? $pomo->todo->title : 'Select a Todo' }}</option>
@if(!$editing)
@foreach($incomplete_todos as $todo) @foreach($incomplete_todos as $todo)
<option value="{{ $todo['id'] }}">{{ $todo['title'] }}</option> <option value="{{ $todo['id'] }}">{{ $todo['title'] }}</option>
@endforeach @endforeach
@endif
</select> </select>
@error('todo_id') @error('todo_id')
<div class="text-red-500 mt-2 text-sm"> <div class="text-red-500 mt-2 text-sm">

View File

@ -1,46 +1,65 @@
@foreach ($pomos as $pomo) <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 max-w-7xl mx-auto py-3">
<tr class="hover:bg-blue-100 dark:hover:bg-gray-700"> @foreach ($pomos as $pomo)
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100"> <div class="border rounded-lg p-4 shadow-md hover:bg-blue-100 dark:hover:bg-gray-700">
<a href="{{ route('project.todo.edit', ['project' => $pomo->todo->project->id, 'todo' => $pomo->todo->id]) }}"> <div class="mb-2">
{{ $pomo->todo->title }} <a href="{{ route('project.todo.edit', ['project' => $pomo->todo->project->id, 'todo' => $pomo->todo->id]) }}"
</a> class="text-blue-900 dark:text-gray-100 font-bold hover:underline">
</td> {{ $pomo->todo->title }}
<!-- Pomo Start and Pomo End --> </a>
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
{{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_start)->format('Y-m-d H:i:s') }}
</td>
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
{{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_end)->format('Y-m-d H:i:s') }}
</td>
<!-- Duration -->
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
{{
\Carbon\Carbon::createFromTimestamp($pomo->pomo_start)->diffInMinutes(\Carbon\Carbon::createFromTimestamp($pomo->pomo_end))
}}
</td>
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
<!-- Truncate notes to 32 characters -->
<div class="max-w-sm truncate">
{{ $pomo->notes }}
</div> </div>
</td> <div class="mb-2 text-blue-900 dark:text-gray-100">
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100"> <strong>Start:</strong> {{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_start)->format('d/m/Y H:i') }}
<!-- Edit and Delete form button groups -->
<div class="flex flex-row">
<div class="flex flex-col">
<a href="{{ route('pomo.edit', $pomo->id) }}"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Edit</a>
</div>
<div class="flex flex-col">
<form action="{{ route('pomo.destroy', $pomo->id) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit"
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Delete
</button>
</form>
</div>
</div> </div>
</td> <div class="mb-2 text-blue-900 dark:text-gray-100">
</tr> <strong>End:</strong> {{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_end)->format('d/m/Y H:i') }}
@endforeach </div>
<div class="mb-2 text-blue-900 dark:text-gray-100">
<strong>Duration:</strong>
{{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_end)
->diff(\Carbon\Carbon::createFromTimestamp($pomo->pomo_start))
->format('%H h %I m')
}}
</div>
<div class="mb-2 truncate text-blue-900 dark:text-gray-100">
<strong>Notes:</strong> {{ $pomo->notes }}
</div>
<div class="flex justify-between">
<a href="{{ route('pomo.edit', $pomo->id) }}"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Edit</a>
<form action="{{ route('pomo.destroy', $pomo->id) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit"
class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Delete
</button>
</form>
</div>
</div>
@endforeach
</div>
@if($pomos->hasMorePages())
<div class="invisible">
<button wire:click.prevent="loadMore"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Load more
</button>
</div>
<!-- Livewire script to trigger the load more button after user scrolls to the bottom of the page -->
<script>
window.onscroll = function(ev) {
if ($(window).scrollTop() + $(window).height() >= $(document).height() - 100) {
Livewire.emit('load-more');
console.log('Load more');
}
};
</script>
@else
<!-- If there are no more pages, show this message -->
<div class="text-blue-900 dark:text-gray-100 font-bold py-2 px-4 rounded flex justify-center">
Congratulations! You've reached the end of the list.
</div>
@endif

View File

@ -0,0 +1,23 @@
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6 mt-4 py-3">
<h2 class="text-2xl font-semibold mb-4 text-indigo-500 dark:text-indigo-400">
<!-- Icon for Target board -->
<svg class="inline-block h-6 w-6 text-indigo-500 dark:text-indigo-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 9v6m0 0v6m0-6h6m-6 0H3"/>
</svg>
Your Stats
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-4">
<livewire:dashboard.pomo-count/>
<livewire:dashboard.pomo-time/>
</div>
</div>

View File

@ -1,17 +1,7 @@
<div class="flex justify-center mt-4">
<table class="table-fixed w-3/4 border rounded-lg dark:bg-gray-800"> <div>
<thead class="bg-blue-100"> <livewire:pomo.pomo-dashboard />
<tr>
<th class="px-4 py-2">Task</th> @include('livewire.pomo.load-pomo')
<th class="px-4 py-2">Start</th>
<th class="px-4 py-2">End</th>
<th class="px-4 py-2">Duration (minutes)</th>
<th class="px-4 py-2">Notes</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody id="pomo-container">
@include('livewire.pomo.load-pomo')
</tbody>
</table>
</div> </div>

View File

@ -31,8 +31,9 @@ class PomoCRUDTest extends TestCase
public function test_user_can_create_pomo(): void public function test_user_can_create_pomo(): void
{ {
$now = now(); // Time in datetime-local format
$end = now()->addMinutes(25); $now = date('Y-m-d\TH:i');
$end = date('Y-m-d\TH:i', strtotime('+25 minutes'));
// Create a pomo through POST and store it in the pomo property // Create a pomo through POST and store it in the pomo property
$response = $this->post(route('pomo.store'), [ $response = $this->post(route('pomo.store'), [
'todo_id' => $this->todo->id, 'todo_id' => $this->todo->id,
@ -64,8 +65,8 @@ class PomoCRUDTest extends TestCase
public function test_user_can_update_pomo_with_authorsation(): void public function test_user_can_update_pomo_with_authorsation(): void
{ {
$this->test_user_can_create_pomo(); $this->test_user_can_create_pomo();
$now = now(); $now = date('Y-m-d\TH:i');
$end = now()->addMinutes(25); $end = date('Y-m-d\TH:i', strtotime('+25 minutes'));
$response = $this->put(route('pomo.update', $this->pomo->id), [ $response = $this->put(route('pomo.update', $this->pomo->id), [
'todo_id' => $this->todo->id, 'todo_id' => $this->todo->id,
'notes' => 'Test Notes Updated', 'notes' => 'Test Notes Updated',