mirror of https://github.com/Devoalda/LaDo.git
refactor(Infinite Loading):
Modified Pomo dashboard to support mobile devices & Infinite loading Added Request Validation for Pomo
This commit is contained in:
parent
99d37aafb6
commit
49ad03f1cb
|
@ -21,7 +21,7 @@ class DashboardController extends Controller
|
|||
->whereDate('due_end', '<=', strtotime('today midnight'))
|
||||
->whereNull('completed_at')
|
||||
->orderBy('due_end', 'asc')
|
||||
->paginate(5);
|
||||
->paginate(5, $columns = ['*'], $pageName = 'todos');
|
||||
|
||||
$todos->transform(function ($todo) {
|
||||
return \App\Models\Todo::find($todo->id);
|
||||
|
|
|
@ -46,10 +46,10 @@ class PomoController extends Controller
|
|||
|
||||
// Convert due_start and end to unix timestamp and save
|
||||
$pomo = new Pomo();
|
||||
$pomo->todo_id = $request->todo_id;
|
||||
$pomo->pomo_start = strtotime($request->pomo_start);
|
||||
$pomo->pomo_end = strtotime($request->pomo_end);
|
||||
$pomo->notes = $request->notes;
|
||||
$pomo->todo_id = $request->safe()->todo_id;
|
||||
$pomo->pomo_start = strtotime($request->safe()->pomo_start);
|
||||
$pomo->pomo_end = strtotime($request->safe()->pomo_end);
|
||||
$pomo->notes = $request->safe()->notes;
|
||||
$pomo->save();
|
||||
|
||||
return redirect()->route('pomo.index')
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
|||
use App\Http\Requests\Project\StoreProjectRequest;
|
||||
use App\Http\Requests\Project\UpdateProjectRequest;
|
||||
use App\Models\Project;
|
||||
use App\Models\Todo;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
@ -26,9 +27,11 @@ class ProjectController extends Controller
|
|||
$user = User::find(auth()->user()->id);
|
||||
$projects = $user->projects()->paginate(4);
|
||||
// Aggregate all todos for all projects
|
||||
$todos = $projects->map(function ($project) {
|
||||
return $project->todos;
|
||||
})->flatten();
|
||||
|
||||
$todos = $user->todos()
|
||||
->map(function ($todo) {
|
||||
return Todo::find($todo->id);
|
||||
});
|
||||
|
||||
if ($request->ajax()){
|
||||
$view = view('project.load-projects', compact('projects'))->render();
|
||||
|
@ -41,7 +44,9 @@ class ProjectController extends Controller
|
|||
return view('project.index', [
|
||||
'projects' => $projects,
|
||||
'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(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,11 @@ use App\Models\{
|
|||
Project,
|
||||
Todo
|
||||
};
|
||||
use Carbon\Carbon;
|
||||
|
||||
class PomoTime extends Component
|
||||
{
|
||||
public int $ave_pomo_time = 0;
|
||||
public $ave_pomo_time = 0;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
@ -35,6 +36,9 @@ class PomoTime extends Component
|
|||
|
||||
$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');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -3,11 +3,9 @@
|
|||
namespace App\Http\Livewire\Pomo;
|
||||
|
||||
use App\Models\Pomo;
|
||||
use App\Models\Project;
|
||||
use App\Models\Todo;
|
||||
use App\Models\User;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class Create extends Component
|
||||
{
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Livewire\Pomo;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
use App\Models\{
|
||||
Pomo,
|
||||
|
@ -12,26 +13,29 @@ use App\Models\{
|
|||
|
||||
class Pomos extends Component
|
||||
{
|
||||
public $perPage = 10;
|
||||
public $perPage = 9;
|
||||
|
||||
public $listeners = [
|
||||
'load-more' => 'loadMore',
|
||||
];
|
||||
|
||||
public function loadMore()
|
||||
{
|
||||
$this->perPage += 10;
|
||||
$this->perPage += 9;
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
$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', [
|
||||
'pomos' => $pomos,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class StorePomoRequest extends FormRequest
|
||||
{
|
||||
|
@ -23,9 +24,16 @@ class StorePomoRequest extends FormRequest
|
|||
{
|
||||
return [
|
||||
'todo_id' => 'required|exists:todos,id',
|
||||
'pomo_start' => 'required|date',
|
||||
'pomo_end' => 'required|date',
|
||||
'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',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'todo_id.required' => 'Please select a todo.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,9 @@ class UpdatePomoRequest extends FormRequest
|
|||
public function rules(): array
|
||||
{
|
||||
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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@ class DatabaseSeeder extends Seeder
|
|||
public function run(): void
|
||||
{
|
||||
\App\Models\User::factory(3)
|
||||
->has(\App\Models\Project::factory()->count(3)
|
||||
->has(\App\Models\Todo::factory()->count(10)
|
||||
->has(\App\Models\Pomo::factory()->count(4))
|
||||
->has(\App\Models\Project::factory()->count(1)
|
||||
->has(\App\Models\Todo::factory()->count(5)
|
||||
->has(\App\Models\Pomo::factory()->count(20))
|
||||
))
|
||||
->create();
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<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">
|
||||
<!-- SVG for Pomo Average Count -->
|
||||
<svg class="inline-block h-6 w-6 text-blue-500 dark:text-blue-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="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
Average Pomos per Project
|
||||
</h3>
|
||||
<p class="text-3xl font-bold text-gray-800 dark:text-gray-100">
|
||||
{{ $ave_pomo_count }}
|
||||
</p>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-4 text-blue-500 dark:text-blue-400">
|
||||
<!-- SVG for Pomo Average Count -->
|
||||
<svg class="inline-block h-6 w-6 text-blue-500 dark:text-blue-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="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
Average Pomos per Project
|
||||
</h3>
|
||||
<p class="text-3xl font-bold text-gray-800 dark:text-gray-100">
|
||||
{{ $ave_pomo_count }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
Average Pomos time spent per Project
|
||||
Average time spent per Project
|
||||
</h3>
|
||||
<p class="text-3xl font-bold text-gray-800 dark:text-gray-100">
|
||||
{{ $ave_pomo_time }}
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
<div class="mb-4">
|
||||
<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"
|
||||
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>
|
||||
@if(!$editing)
|
||||
@foreach($incomplete_todos as $todo)
|
||||
<option value="{{ $todo['id'] }}">{{ $todo['title'] }}</option>
|
||||
@endforeach
|
||||
@endif
|
||||
</select>
|
||||
@error('todo_id')
|
||||
<div class="text-red-500 mt-2 text-sm">
|
||||
|
|
|
@ -1,46 +1,65 @@
|
|||
@foreach ($pomos as $pomo)
|
||||
<tr class="hover:bg-blue-100 dark:hover:bg-gray-700">
|
||||
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
|
||||
<a href="{{ route('project.todo.edit', ['project' => $pomo->todo->project->id, 'todo' => $pomo->todo->id]) }}">
|
||||
{{ $pomo->todo->title }}
|
||||
</a>
|
||||
</td>
|
||||
<!-- Pomo Start and Pomo End -->
|
||||
<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 class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 max-w-7xl mx-auto py-3">
|
||||
@foreach ($pomos as $pomo)
|
||||
<div class="border rounded-lg p-4 shadow-md hover:bg-blue-100 dark:hover:bg-gray-700">
|
||||
<div class="mb-2">
|
||||
<a href="{{ route('project.todo.edit', ['project' => $pomo->todo->project->id, 'todo' => $pomo->todo->id]) }}"
|
||||
class="text-blue-900 dark:text-gray-100 font-bold hover:underline">
|
||||
{{ $pomo->todo->title }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="border px-4 py-2 text-blue-900 dark:text-gray-100">
|
||||
<!-- 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 class="mb-2 text-blue-900 dark:text-gray-100">
|
||||
<strong>Start:</strong> {{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_start)->format('d/m/Y H:i') }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
<div class="mb-2 text-blue-900 dark:text-gray-100">
|
||||
<strong>End:</strong> {{ \Carbon\Carbon::createFromTimestamp($pomo->pomo_end)->format('d/m/Y H:i') }}
|
||||
</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
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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">
|
||||
<thead class="bg-blue-100">
|
||||
<tr>
|
||||
<th class="px-4 py-2">Task</th>
|
||||
<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>
|
||||
<livewire:pomo.pomo-dashboard />
|
||||
|
||||
@include('livewire.pomo.load-pomo')
|
||||
</div>
|
||||
|
||||
|
|
|
@ -31,8 +31,9 @@ class PomoCRUDTest extends TestCase
|
|||
|
||||
public function test_user_can_create_pomo(): void
|
||||
{
|
||||
$now = now();
|
||||
$end = now()->addMinutes(25);
|
||||
// Time in datetime-local format
|
||||
$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
|
||||
$response = $this->post(route('pomo.store'), [
|
||||
'todo_id' => $this->todo->id,
|
||||
|
@ -64,8 +65,8 @@ class PomoCRUDTest extends TestCase
|
|||
public function test_user_can_update_pomo_with_authorsation(): void
|
||||
{
|
||||
$this->test_user_can_create_pomo();
|
||||
$now = now();
|
||||
$end = now()->addMinutes(25);
|
||||
$now = date('Y-m-d\TH:i');
|
||||
$end = date('Y-m-d\TH:i', strtotime('+25 minutes'));
|
||||
$response = $this->put(route('pomo.update', $this->pomo->id), [
|
||||
'todo_id' => $this->todo->id,
|
||||
'notes' => 'Test Notes Updated',
|
||||
|
|
Loading…
Reference in New Issue