Lazzerex

Explore
Backend, Systems & Web Development

Home Articles Sử dụng Laragon để tạo một ứng dụng web đơn giản

Sử dụng Laragon để tạo một ứng dụng web đơn giản

H. S. N. Bình -- views

  • Programming

Tạo 1 web application Task Manager bằng framework backend Laravel sử dụng phần mềm Laragon

Cover image for Sử dụng Laragon để tạo một ứng dụng web đơn giản

Tạo 1 web application: Task Manager bằng framework backend Laravel sử dụng phần mềm Laragon

Lưu ý:

  • cần cài đặt Composer để quản lý mấy cái dependency cho thằng PHP
  • cần cài đặt Node.js (bắt buộc để cài tailwind.css), HeidiSQL - không bắt buộc - có thể dùng thẳng PhpMyAdmin cũng được)

Đầu tiên mở Terminal của Laragon và chạy lệnh:

Article image
                  cd C:\laragon\www
                

(dùng Powershell, hoặc git bash CLI cũng được, mà Laragon có sẵn terminal thì ngại gì không sài :v)

Tạo một project Laravel mới bằng cách dùng lệnh “composer create-project”:

Article image
                  composer create-project --prefer-dist laravel/laravel task-manager
                

Còn không thích dùng lệnh cho ngầu thì xài Laragon cài như sau:

Nhấn chuột phải vào phần mềm Laragron trên thanh công cụ, rồi chọn “Quick App”, rồi chọn Laravel

Laragon sẽ mặc định cài Project Laravel mới vào folder www (như hình)

Article image

Sau đó cd vào task-manager:

Article image
                  cd task-manager
                

Dùng lệnh “code .” để mở toàn bộ folder này bằng Visual Studio Code (nên xài cái này cho tiện vì nó hộ trỡ sẵn các extensions cần thiết cho Laravel, như là intelisense các thứ,…)

Article image

Mở file “.env”, cập nhật setting database như sau:

Article image
                  DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=task_manager
DB_USERNAME=root
DB_PASSWORD=
                

Sau đó, tạo một database mới bằng Laragon:

Article image

Trên giao diện chính, chọn vào nút “Database”, nếu bạn cài đặt HeidiSQL rồi thì nó sẽ mở lên như sau:

Article image

Trong giao diện HeidiSQL, bạn chón nút new và tạo một database mới với tên là “task-manager”

Còn nếu không có HeidiSQL thì Laragon sẽ mặc định mở PhpMyAdmin, trong này thì cách tạo database cũng tương tự.

Article image

Chỉ việc nhấn nút new và tạo database mới thôi!

Database đã xong, cài đặt Laravel thành công, bây giờ chúng ta bắt đầu xây dụng hệ thống cho app Task Manager.

Đầu tiên, tạo một model mới và chạy migration cho task của chúng ta như sau. Tiếp tục vào terminal và chạy dòng lệnh:

Article image
                  php artisan make:model Task -m
                

Lệnh này sẽ tạo cho chúng ta một cái Task Model.

Chúng ta phải tạo model vì Laravel sẽ hoạt động theo mô hình MVC (Model - View - Controller)

Article image

Bây giờ, chúng ta sẽ sửa đổi file migration để xác định cấu trúc bảng (table) của chúng ta. Mở file migration có đường dẫn trong root project là: “database/migrations/xxxx_xx_xx_create_tasks_table.php” và thêm đoạn code sau:

Article image
                  public function up()
{
    Schema::create('tasks', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('description')->nullable();
        $table->enum('status', ['pending', 'in_progress', 'completed'])->default('pending');
        $table->date('due_date')->nullable();
        $table->timestamps();
    });
}
                

Vì là thằng Laravel nó hoạt động theo mô hình MVC nên việc tiếp theo hiển nhiên là phải tạo một cái Task Controller rồi :v

Chạy lệnh php artisan để tạo controller như sau:

Article image
                  php artisan make:controller TaskController --resource
                

Lệnh này sẽ tạo giúp chúng ta một cái Controller với đầy đủ các CRUD method luôn (CREATE, READ, UPDATE and DELETE)

Mở “routes/web.php” trong file root lên và thêm đoạn code sau:

Article image
                  use App\Http\Controllers\TaskController;

Route::get('/', function () {
    return redirect()->route('tasks.index');
});

Route::resource('tasks', TaskController::class);
                

Có thể các bạn cũng thấy là chúng ta đang chỉnh cho route đến cái tasks.index, cái đấy là gì thì chúng ta sẽ nói đến sau. (vì kiểu gì chả đụng =)) phải làm cả giao diện mà.)

Tiếp theo, chúng ta sẽ triển khai cái Model cho Task như sau.

Mở file “app/Models/Task.php”, và cập nhật nó như sau:

Article image
                  <?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    use HasFactory;

    // Define which fields can be mass-assigned
    protected $fillable = [
        'title',
        'description',
        'status',
        'due_date'
    ];

    // Define attribute casting
    protected $casts = [
        'due_date' => 'date',
    ];
}
                

Sau đó, chúng ta chạy lệnh migration của PHP để tạo bảng với các nội dung mà ta đã định nghĩa trong code lúc nãy.

Article image
                  php artisan migrate
                

Bây giớ, khi các bạn quay lại HeidiSQL, hoặc PHPMyAdmin rồi reload thì các bạn sẽ thấy bảng đã được tạo rồi, tất nhiên là chưa có data gì cả.

Lưu ý: nếu bạn muốn tạo data mẫu, có thể tham khảo lệnh seed database.

Tiếp theo, chúng ta sẽ triển khai các phương cho Controller

Mở “app/Http/Controllers/TaskController.php” và cập nhật nó như sau:

(code dài lắm nên nhét sẽ nhét trong file này nhé, thông cảm :v)

                  <?php

namespace App\Http\Controllers;

use App\Models\Task;
use Illuminate\Http\Request;

class TaskController extends Controller
{
    /**
     * Display a listing of all tasks.
     */
    public function index()
    {
        // Retrieve all tasks, ordered by creation date (newest first)
        $tasks = Task::latest()->get();
        
        // Pass the tasks to the view
        return view('tasks.index', compact('tasks'));
    }

    /**
     * Show the form for creating a new task.
     */
    public function create()
    {
        // Simply return the view containing the task creation form
        return view('tasks.create');
    }

    /**
     * Store a newly created task in the database.
     */
    public function store(Request $request)
    {
        // Validate the incoming request data
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'required|in:pending,in_progress,completed',
            'due_date' => 'nullable|date',
        ]);
        
        // Create a new task with the validated data
        Task::create($validated);
        
        // Redirect to the tasks index page with a success message
        return redirect()->route('tasks.index')
            ->with('success', 'Task created successfully!');
    }

    /**
     * Display the specified task.
     */
    public function show(Task $task)
    {
        // Pass the task to the view
        return view('tasks.show', compact('task'));
    }

    /**
     * Show the form for editing the specified task.
     */
    public function edit(Task $task)
    {
        // Pass the task to the edit form view
        return view('tasks.edit', compact('task'));
    }

    /**
     * Update the specified task in the database.
     */
    public function update(Request $request, Task $task)
    {
        // Validate the incoming request data
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'description' => 'nullable|string',
            'status' => 'required|in:pending,in_progress,completed',
            'due_date' => 'nullable|date',
        ]);
        
        // Update the task with the validated data
        $task->update($validated);
        
        // Redirect to the task's detail page with a success message
        return redirect()->route('tasks.show', $task)
            ->with('success', 'Task updated successfully!');
    }

    /**
     * Remove the specified task from the database.
     */
    public function destroy(Task $task)
    {
        // Delete the task
        $task->delete();
        
        // Redirect to the tasks index page with a success message
        return redirect()->route('tasks.index')
            ->with('success', 'Task deleted successfully!');
    }
}
                

Tiếp theo, để cho người dùng nhìn và dùng được thì chúng sẽ đi tạo Views

Nếu chưa có sẵn thì bạn sẽ tạo một file mới với đường dẫn như sau:

“resources/views/layouts/app.blade.php”. Đây sẽ là cái view layout của chúng ta.

Rồi, trong app.blade.php thì các bạn code HTML như sau:

                  <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Manager</title>
    <!-- Include Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        body {
            padding-top: 20px;
        }
        .task-status-pending {
            color: orange;
        }
        .task-status-in_progress {
            color: blue;
        }
        .task-status-completed {
            color: green;
        }
    </style>
</head>
<body>
    <div class="container">
        <header class="mb-4">
            <h1 class="text-center">Laravel Task Manager</h1>
            <nav class="navbar navbar-expand-lg navbar-light bg-light">
                <div class="container-fluid">
                    <div class="collapse navbar-collapse" id="navbarNav">
                        <ul class="navbar-nav">
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('tasks.index') }}">All Tasks</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="{{ route('tasks.create') }}">Create New Task</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>
        </header>

        <!-- Display flash messages -->
        @if(session('success'))
            <div class="alert alert-success alert-dismissible fade show" role="alert">
                {{ session('success') }}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            </div>
        @endif

        @if(session('error'))
            <div class="alert alert-danger alert-dismissible fade show" role="alert">
                {{ session('error') }}
                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
            </div>
        @endif

        <main>
            @yield('content')
        </main>

        <footer class="mt-5 text-center text-muted">
            <p>&copy; {{ date('Y') }} Laravel Task Manager</p>
        </footer>
    </div>

    <!-- Include Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
                

Đã có view layout, giờ chúng ta sẽ đi tạo những view cụ thể khác cho người dùng, bao gồm Index, Create, Show và Edit:

Trong file “resources/views/tasks/index.blade.php”

                  @extends('layouts.app')

@section('content')
    <div class="card">
        <div class="card-header d-flex justify-content-between align-items-center">
            <h2>Task List</h2>
            <a href="{{ route('tasks.create') }}" class="btn btn-primary">Create New Task</a>
        </div>
        <div class="card-body">
            @if($tasks->isEmpty())
                <div class="alert alert-info">
                    No tasks found. Create your first task!
                </div>
            @else
                <div class="table-responsive">
                    <table class="table table-striped">
                        <thead>
                            <tr>
                                <th>ID</th>
                                <th>Title</th>
                                <th>Status</th>
                                <th>Due Date</th>
                                <th>Actions</th>
                            </tr>
                        </thead>
                        <tbody>
                            @foreach($tasks as $task)
                                <tr>
                                    <td>{{ $task->id }}</td>
                                    <td>{{ $task->title }}</td>
                                    <td>
                                        <span class="badge bg-{{ $task->status == 'pending' ? 'warning' : ($task->status == 'in_progress' ? 'primary' : 'success') }}">
                                            {{ ucfirst(str_replace('_', ' ', $task->status)) }}
                                        </span>
                                    </td>
                                    <td>{{ $task->due_date ? $task->due_date->format('M d, Y') : 'Not set' }}</td>
                                    <td>
                                        <div class="btn-group" role="group">
                                            <a href="{{ route('tasks.show', $task) }}" class="btn btn-sm btn-info">View</a>
                                            <a href="{{ route('tasks.edit', $task) }}" class="btn btn-sm btn-warning">Edit</a>
                                            <form action="{{ route('tasks.destroy', $task) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this task?');">
                                                @csrf
                                                @method('DELETE')
                                                <button type="submit" class="btn btn-sm btn-danger">Delete</button>
                                            </form>
                                        </div>
                                    </td>
                                </tr>
                            @endforeach
                        </tbody>
                    </table>
                </div>
            @endif
        </div>
    </div>
@endsection
                

(resources/views/tasks/create.blade.php):

                  @extends('layouts.app')

@section('content')
    <div class="card">
        <div class="card-header">
            <h2>Create New Task</h2>
        </div>
        <div class="card-body">
            <form action="{{ route('tasks.store') }}" method="POST">
                @csrf
                
                <div class="mb-3">
                    <label for="title" class="form-label">Title</label>
                    <input type="text" class="form-control @error('title') is-invalid @enderror" id="title" name="title" value="{{ old('title') }}" required>
                    @error('title')
                        <div class="invalid-feedback">{{ $message }}</div>
                    @enderror
                </div>
                
                <div class="mb-3">
                    <label for="description" class="form-label">Description</label>
                    <textarea class="form-control @error('description') is-invalid @enderror" id="description" name="description" rows="3">{{ old('description') }}</textarea>
                    @error('description')
                        <div class="invalid-feedback">{{ $message }}</div>
                    @enderror
                </div>
                
                <div class="mb-3">
                    <label for="status" class="form-label">Status</label>
                    <select class="form-select @error('status') is-invalid @enderror" id="status" name="status" required>
                        <option value="pending" {{ old('status') == 'pending' ? 'selected' : '' }}>Pending</option>
                        <option value="in_progress" {{ old('status') == 'in_progress' ? 'selected' : '' }}>In Progress</option>
                        <option value="completed" {{ old('status') == 'completed' ? 'selected' : '' }}>Completed</option>
                    </select>
                    @error('status')
                        <div class="invalid-feedback">{{ $message }}</div>
                    @enderror
                </div>
                
                <div class="mb-3">
                    <label for="due_date" class="form-label">Due Date</label>
                    <input type="date" class="form-control @error('due_date') is-invalid @enderror" id="due_date" name="due_date" value="{{ old('due_date') }}">
                    @error('due_date')
                        <div class="invalid-feedback">{{ $message }}</div>
                    @enderror
                </div>
                
                <div class="d-flex justify-content-between">
                    <a href="{{ route('tasks.index') }}" class="btn btn-secondary">Cancel</a>
                    <button type="submit" class="btn btn-primary">Create Task</button>
                </div>
            </form>
        </div>
    </div>
@endsection
                

(resources/views/tasks/show.blade.php):

                  @extends('layouts.app')

@section('content')
    <div class="card">
        <div class="card-header d-flex justify-content-between align-items-center">
            <h2>Task Details</h2>
            <div>
                <a href="{{ route('tasks.edit', $task) }}" class="btn btn-warning">Edit Task</a>
                <a href="{{ route('tasks.index') }}" class="btn btn-secondary">Back to List</a>
            </div>
        </div>
        <div class="card-body">
            <div class="row mb-4">
                <div class="col-md-8">
                    <h3>{{ $task->title }}</h3>
                </div>
                <div class="col-md-4 text-md-end">
                    <span class="badge bg-{{ $task->status == 'pending' ? 'warning' : ($task->status == 'in_progress' ? 'primary' : 'success') }} fs-6">
                        {{ ucfirst(str_replace('_', ' ', $task->status)) }}
                    </span>
                </div>
            </div>
            
            <div class="row mb-3">
                <div class="col-md-3 fw-bold">Created:</div>
                <div class="col-md-9">{{ $task->created_at->format('F d, Y \a\t h:i A') }}</div>
            </div>
            
            <div class="row mb-3">
                <div class="col-md-3 fw-bold">Last Updated:</div>
                <div class="col-md-9">{{ $task->updated_at->format('F d, Y \a\t h:i A') }}</div>
            </div>
            
            <div class="row mb-3">
                <div class="col-md-3 fw-bold">Due Date:</div>
                <div class="col-md-9">{{ $task->due_date ? $task->due_date->format('F d, Y') : 'Not set' }}</div>
            </div>
            
            <div class="row mb-4">
                <div class="col-md-3 fw-bold">Description:</div>
                <div class="col-md-9">
                    {{ $task->description ?? 'No description provided.' }}
                </div>
            </div>
            
            <hr>
            
            <div class="d-flex justify-content-between mt-3">
                <a href="{{ route('tasks.index') }}" class="btn btn-secondary">Back to List</a>
                <div>
                    <a href="{{ route('tasks.edit', $task) }}" class="btn btn-warning">Edit</a>
                    <form action="{{ route('tasks.destroy', $task) }}" method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this task?');">
                        @csrf
                        @method('DELETE')
                        <button type="submit" class="btn btn-danger">Delete</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
@endsection
                

(resources/views/tasks/edit.blade.php):

                  @extends('layouts.app')

@section('content')
    <div class="card">
        <div class="card-header">
            <h2>Edit Task</h2>
        </div>
        <div class="card-body">
            <form action="{{ route('tasks.update', $task) }}" method="POST">
                @csrf
                @method('PUT')
                
                <div class="mb-3">
                    <label for="title" class="form-label">Title</label>
                    <input type="text" class="form-control @error('title') is-invalid @enderror" id="title" name="title" value="{{ old('title', $task->title) }}" required>
                    @error('title')
                        <div class="invalid-feedback">{{ $message }}</div>
                    @enderror
                </div>
                
                <div class="mb-3">
                    <label for="description" class="form-label">Description</label>
                    <textarea class="form-control @error('description') is-invalid @enderror" id="description" name="description" rows="3">{{ old('description', $task->description) }}</textarea>
                    @error('description')
                        <div class="invalid-feedback">{{ $message }}</div>
                    @enderror
                </div>
                
                <div class="mb-3">
                    <label for="status" class="form-label">Status</label>
                    <select class="form-select @error('status') is-invalid @enderror" id="status" name="status" required>
                        <option value="pending" {{ old('status', $task->status) == 'pending' ? 'selected' : '' }}>Pending</option>
                        <option value="in_progress" {{ old('status', $task->status) == 'in_progress' ? 'selected' : '' }}>In Progress</option>
                        <option value="completed" {{ old('status', $task->status) == 'completed' ? 'selected' : '' }}>Completed</option>
                    </select>
                    @error('status')
                        <div class="invalid-feedback">{{ $message }}</div>
                    @enderror
                </div>
                
                <div class="mb-3">
                    <label for="due_date" class="form-label">Due Date</label>
                    <input type="date" class="form-control @error('due_date') is-invalid @enderror" id="due_date" name="due_date" value="{{ old('due_date', $task->due_date ? $task->due_date->format('Y-m-d') : '') }}">
                    @error('due_date')
                        <div class="invalid-feedback">{{ $message }}</div>
                    @enderror
                </div>
                
                <div class="d-flex justify-content-between">
                    <a href="{{ route('tasks.show', $task) }}" class="btn btn-secondary">Cancel</a>
                    <button type="submit" class="btn btn-primary">Update Task</button>
                </div>
            </form>
        </div>
    </div>
@endsection
                

Vậy là cơ bản chúng ta đã xong một cái ứng dụng WEB mà ứng dụng mô hình MVC và CRUD rồi đó các bạn :D. Tuy nhiên, để cho nó được đẹp hơn thì chúng ta cần phải làm theo bước nữa.

À quên, để xem thử ứng dụng web của chúng ta thì các bạn chỉ việc nháy chuột phải vào Laragon, vào chỗ “www”, rồi bấm vào task-manager thì ứng dụng web của chúng ta sẽ hiện lên nha

Article image

Vậy thì bây giờ, chúng ta hãy thử cải thiện cái UI bằng Tailwind.css nha 😀

Tailwind CSS là gì ?

Thì nó là một utility-first CSS framework, nó cũng giống như Bootstrap, nó có những class built-in mà chúng ta có thể dùng. Tailwind CSS có nhiều các class bao gồm các thuộc tính CSS khác nhau và quan trọng, chúng ta có thể dễ dàng mở rộng tạo mới ra những class bằng chính những class của nó.

Tại sao chúng ta nên dùng Tailwind ?

Nói chung là nó cũng na ná Boostrap thôi nhưng một điều tiện lợi khi dùng framework này là chúng ta có nhiều class mới hơn tiện lợi hơn Boostrap. Các bạn có thể tham khảo tại đây trong Tailwind. Và hơn nữa, việc có nhiều thêm những class nhưng với quy tắc đặt tên cực kỳ thân thiện với người dùng, người dùng cũng có thể nhìn vào class đó và có thể biết được class này nó đang style cái gì. Chúng ta cũng phải nói đến khả năng tùy biến và mở rộng cao, đem đến cho ta sự linh hoạt không giới hạn.

Rồi bước đầu tiên là các bạn phải cài Node.js nha, rồi sau đó vào terminal của Largon và chạy câu lệnh sau:

Article image
                  # Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
                

Sau khi đã cài đặt thành công rồi, thì bây giờ các bạn hãy vào root folder của project các bạn và check xem liệu đã có file “tailwind.config.cjs” hay chưa. Nếu chưa có thì các bạn sẽ tạo file này và code như sau:

Article image
                  /** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./resources/**/*.blade.php",
    "./resources/**/*.js",
    "./resources/**/*.vue",
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
}
                

sau đó, các bạn cũng check xem liệu project chúng ta có file là “postcss.config.cjs” hay chưa, nếu chưa có thì chúng ta sẽ tạo file mới và thêm code như sau:

Article image
                  module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
                

Nếu nó báo lỗi, thì các bạn hãy chạy lệnh uninstall cái form mà các bạn vừa cài, sau đó các bạn tiến hành cài forms của Tailwind.css theo lệnh mới như này và tiếp tục 2 bước phía trên là ok:

Article image
                  npm install -D tailwindcss@3.3.2 postcss@8.4.23 autoprefixer@10.4.14
                
                  npm install -D @tailwindcss/forms
                

sau đó chúng ta sẽ thêm cái Tailwind directives vào css của project theo đường dẫn này: “resources/css/app.css” với code như sau:

                  @tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom styles can go here */
.task-card {
  @apply transition-all duration-300 hover:shadow-lg;
}

.btn-primary {
  @apply px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors;
}

.btn-secondary {
  @apply px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors;
}

.btn-danger {
  @apply px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors;
}

/* Status color scheme */
.status-pending {
  @apply bg-yellow-100 text-yellow-800 border-yellow-200;
}

.status-in_progress {
  @apply bg-blue-100 text-blue-800 border-blue-200;
}

.status-completed {
  @apply bg-green-100 text-green-800 border-green-200;
}
                

sau đó, các bạn có thể tiến hành compile các assests mà chúng ta vừa cài như sau:

Article image

Tiếp theo, chúng ta sẽ đi thiết kế lại toàn bộ cái layout lúc nãy.

“resources/views/layouts/app.blade.php”:

                  <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TaskFlow - Manage Your Tasks Efficiently</title>
    <!-- Styles -->
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    <!-- Fonts -->
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
    <!-- Icons (Heroicons) -->
    <script src="https://unpkg.com/heroicons@1.0.6/outline"></script>
</head>
<body class="bg-gray-50 min-h-screen font-sans text-gray-800">
    <div class="flex flex-col min-h-screen">
        <header class="bg-white shadow">
            <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                <div class="flex justify-between h-16">
                    <div class="flex">
                        <div class="flex-shrink-0 flex items-center">
                            <a href="{{ route('tasks.index') }}" class="text-2xl font-bold text-blue-600">
                                TaskFlow
                            </a>
                        </div>
                        <nav class="ml-6 flex space-x-4 items-center">
                            <a href="{{ route('tasks.index') }}" class="px-3 py-2 rounded-md text-sm font-medium {{ request()->routeIs('tasks.index') ? 'bg-blue-100 text-blue-800' : 'text-gray-600 hover:text-gray-900' }}">
                                Dashboard
                            </a>
                            <a href="{{ route('tasks.create') }}" class="px-3 py-2 rounded-md text-sm font-medium {{ request()->routeIs('tasks.create') ? 'bg-blue-100 text-blue-800' : 'text-gray-600 hover:text-gray-900' }}">
                                New Task
                            </a>
                        </nav>
                    </div>
                </div>
            </div>
        </header>

        <!-- Flash Messages -->
        @if(session('success'))
            <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
                <div class="bg-green-100 border border-green-200 text-green-800 px-4 py-3 rounded relative" role="alert">
                    <span class="block sm:inline">{{ session('success') }}</span>
                    <button type="button" class="absolute top-0 bottom-0 right-0 px-4 py-3" onclick="this.parentElement.style.display='none'">
                        <svg class="h-5 w-5 text-green-800" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                        </svg>
                    </button>
                </div>
            </div>
        @endif

        @if(session('error'))
            <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4">
                <div class="bg-red-100 border border-red-200 text-red-800 px-4 py-3 rounded relative" role="alert">
                    <span class="block sm:inline">{{ session('error') }}</span>
                    <button type="button" class="absolute top-0 bottom-0 right-0 px-4 py-3" onclick="this.parentElement.style.display='none'">
                        <svg class="h-5 w-5 text-red-800" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                        </svg>
                    </button>
                </div>
            </div>
        @endif

        <main class="flex-grow max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6">
            @yield('content')
        </main>

        <footer class="bg-white border-t border-gray-200 py-6">
            <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                <div class="flex justify-between items-center">
                    <div class="text-sm text-gray-500">
                        &copy; {{ date('Y') }} TaskFlow. All rights reserved.
                    </div>
                    <div class="text-sm text-gray-500">
                        Built with Laravel
                    </div>
                </div>
            </div>
        </footer>
    </div>
</body>
</html>
                

“resources/views/tasks/index.blade.php”:

                  @extends('layouts.app')

@section('content')
    <div class="mb-6 flex justify-between items-center">
        <h1 class="text-2xl font-bold text-gray-900">My Tasks</h1>
        <a href="{{ route('tasks.create') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 active:bg-blue-800 focus:outline-none focus:border-blue-800 focus:ring ring-blue-300 disabled:opacity-25 transition">
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" 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>
            New Task
        </a>
    </div>

    <!-- Task Filter Controls (Optional) -->
    <div class="bg-white rounded-lg shadow-sm p-4 mb-6">
        <div class="flex flex-wrap items-center gap-4">
            <div class="flex items-center">
                <span class="text-sm font-medium text-gray-700 mr-2">Filter by Status:</span>
                <select class="rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
                    <option value="">All Tasks</option>
                    <option value="pending">Pending</option>
                    <option value="in_progress">In Progress</option>
                    <option value="completed">Completed</option>
                </select>
            </div>
            <div class="relative flex-grow max-w-sm">
                <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
                    <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
                    </svg>
                </div>
                <input type="search" class="pl-10 rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50 w-full" placeholder="Search tasks...">
            </div>
        </div>
    </div>

    @if($tasks->isEmpty())
        <div class="bg-white rounded-lg shadow-sm p-6 text-center">
            <svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path>
            </svg>
            <h3 class="text-lg font-medium text-gray-900 mb-2">No tasks yet</h3>
            <p class="text-gray-500 mb-4">Get started by creating your first task</p>
            <a href="{{ route('tasks.create') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 active:bg-blue-800 focus:outline-none focus:border-blue-800 focus:ring ring-blue-300 disabled:opacity-25 transition">
                Create Task
            </a>
        </div>
    @else
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            @foreach($tasks as $task)
                <div class="bg-white rounded-lg shadow-sm overflow-hidden border border-gray-200 hover:shadow-md transition-shadow task-card">
                    <div class="px-4 py-5 sm:p-6">
                        <div class="flex justify-between items-start">
                            <h3 class="text-lg font-medium text-gray-900 truncate mb-1">{{ $task->title }}</h3>
                            <span class="px-2 py-1 text-xs font-medium rounded-full {{ $task->status == 'pending' ? 'bg-yellow-100 text-yellow-800' : ($task->status == 'in_progress' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800') }}">
                                {{ ucfirst(str_replace('_', ' ', $task->status)) }}
                            </span>
                        </div>
                        
                        @if($task->description)
                            <p class="mt-2 text-sm text-gray-500 line-clamp-2">{{ $task->description }}</p>
                        @endif
                        
                        <div class="mt-4 flex items-center text-sm text-gray-500">
                            <svg class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
                            </svg>
                            <span>{{ $task->due_date ? $task->due_date->format('M d, Y') : 'No due date' }}</span>
                        </div>
                    </div>
                    
                    <div class="bg-gray-50 px-4 py-4 sm:px-6 border-t border-gray-200">
                        <div class="flex justify-between items-center">
                            <div class="text-xs text-gray-500">
                                Created {{ $task->created_at->diffForHumans() }}
                            </div>
                            <div class="flex space-x-2">
                                <a href="{{ route('tasks.show', $task) }}" class="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                                    View
                                </a>
                                <a href="{{ route('tasks.edit', $task) }}" class="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                                    Edit
                                </a>
                            </div>
                        </div>
                    </div>
                </div>
            @endforeach
        </div>
    @endif
@endsection
                

“resources/views/tasks/create.blade.php”:

                  @extends('layouts.app')

@section('content')
    <div class="max-w-2xl mx-auto">
        <div class="md:flex md:items-center md:justify-between mb-6">
            <div class="flex-1 min-w-0">
                <h1 class="text-2xl font-bold text-gray-900">Create New Task</h1>
                <p class="mt-1 text-sm text-gray-500">Add a new task to your list</p>
            </div>
            <div class="mt-4 md:mt-0 md:ml-4">
                <a href="{{ route('tasks.index') }}" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                    <svg class="-ml-1 mr-2 h-5 w-5 text-gray-500" 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="M11 17l-5-5m0 0l5-5m-5 5h12" />
                    </svg>
                    Back to Tasks
                </a>
            </div>
        </div>

        <div class="bg-white shadow overflow-hidden sm:rounded-lg">
            <form action="{{ route('tasks.store') }}" method="POST">
                @csrf
                
                <div class="px-4 py-5 sm:p-6">
                    <div class="grid grid-cols-6 gap-6">
                        <div class="col-span-6">
                            <label for="title" class="block text-sm font-medium text-gray-700">Task Title</label>
                            <input type="text" name="title" id="title" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md @error('title') border-red-300 text-red-900 placeholder-red-300 @enderror" placeholder="Enter task title" value="{{ old('title') }}" required>
                            @error('title')
                                <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
                            @enderror
                        </div>

                        <div class="col-span-6">
                            <label for="description" class="block text-sm font-medium text-gray-700">Description</label>
                            <textarea name="description" id="description" rows="4" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md @error('description') border-red-300 text-red-900 placeholder-red-300 @enderror" placeholder="Enter task description">{{ old('description') }}</textarea>
                            @error('description')
                                <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
                            @enderror
                        </div>

                        <div class="col-span-6 sm:col-span-3">
                            <label for="status" class="block text-sm font-medium text-gray-700">Status</label>
                            <select id="status" name="status" class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
                                <option value="pending" {{ old('status') == 'pending' ? 'selected' : '' }}>Pending</option>
                                <option value="in_progress" {{ old('status') == 'in_progress' ? 'selected' : '' }}>In Progress</option>
                                <option value="completed" {{ old('status') == 'completed' ? 'selected' : '' }}>Completed</option>
                            </select>
                            @error('status')
                                <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
                            @enderror
                        </div>

                        <div class="col-span-6 sm:col-span-3">
                            <label for="due_date" class="block text-sm font-medium text-gray-700">Due Date</label>
                            <input type="date" name="due_date" id="due_date" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md" value="{{ old('due_date') }}">
                            @error('due_date')
                                <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
                            @enderror
                        </div>
                    </div>
                </div>
                
                <div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
                    <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                        Create Task
                    </button>
                </div>
            </form>
        </div>
    </div>
@endsection
                

“resources/views/tasks/show.blade.php”:

                  @extends('layouts.app')

@section('content')
    <div class="max-w-3xl mx-auto">
        <div class="mb-6 flex justify-between items-center">
            <div>
                <h1 class="text-2xl font-bold text-gray-900">{{ $task->title }}</h1>
                <p class="mt-1 text-sm text-gray-500">
                    Created {{ $task->created_at->format('M d, Y') }} at {{ $task->created_at->format('h:i A') }}
                </p>
            </div>
            <div class="flex space-x-2">
                <a href="{{ route('tasks.edit', $task) }}" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                    <svg class="-ml-1 mr-2 h-5 w-5 text-gray-500" 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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
                    </svg>
                    Edit
                </a>
                <a href="{{ route('tasks.index') }}" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                    <svg class="-ml-1 mr-2 h-5 w-5 text-gray-500" 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="M11 17l-5-5m0 0l5-5m-5 5h12" />
                    </svg>
                    Back
                </a>
            </div>
        </div>

        <div class="bg-white shadow overflow-hidden sm:rounded-lg">
            <div class="px-4 py-5 sm:p-6">
                <div class="flex justify-between items-center mb-6">
                    <span class="px-3 py-1 text-sm font-medium rounded-full {{ $task->status == 'pending' ? 'bg-yellow-100 text-yellow-800' : ($task->status == 'in_progress' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800') }}">
                        {{ ucfirst(str_replace('_', ' ', $task->status)) }}
                    </span>
                    
                    @if($task->due_date)
                        <div class="flex items-center text-sm {{ $task->due_date->isPast() ? 'text-red-500' : 'text-gray-500' }}">
                            <svg class="flex-shrink-0 mr-1.5 h-5 w-5 {{ $task->due_date->isPast() ? 'text-red-400' : 'text-gray-400' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
                            </svg>
                            <span>
                                Due {{ $task->due_date->format('M d, Y') }}
                                @if($task->due_date->isPast()) (Overdue) @endif
                            </span>
                        </div>
                    @else
                        <div class="flex items-center text-sm text-gray-500">
                            <svg class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
                            </svg>
                            <span>No due date set</span>
                        </div>
                    @endif
                </div>

                <div class="prose max-w-none mb-6">
                    <h3 class="text-lg font-medium leading-6 text-gray-900 mb-2">Description</h3>
                    <div class="bg-gray-50 p-4 rounded-lg border border-gray-200">
                        @if($task->description)
                            <p class="text-gray-800 whitespace-pre-line">{{ $task->description }}</p>
                        @else
                            <p class="text-gray-500 italic">No description provided.</p>
                        @endif
                    </div>
                </div>

                <div class="border-t border-gray-200 pt-6">
                    <h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">Activity Timeline</h3>
                    <div class="flow-root">
                        <ul role="list" class="-mb-8">
                            <li>
                                <div class="relative pb-8">
                                    <div class="relative flex space-x-3">
                                        <div>
                                            <span class="h-8 w-8 rounded-full bg-green-500 flex items-center justify-center ring-8 ring-white">
                                                <svg class="h-5 w-5 text-white" 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>
                                            </span>
                                        </div>
                                        <div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
                                            <div>
                                                <p class="text-sm text-gray-500">Task created</p>
                                            </div>
                                            <div class="text-right text-sm whitespace-nowrap text-gray-500">
                                                <time datetime="{{ $task->created_at->format('Y-m-d') }}">{{ $task->created_at->format('M d, Y') }}</time>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </li>
                            <li>
                                <div class="relative pb-8">
                                    <div class="relative flex space-x-3">
                                        <div>
                                            <span class="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center ring-8 ring-white">
                                                <svg class="h-5 w-5 text-white" 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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
                                                </svg>
                                            </span>
                                        </div>
                                        <div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
                                            <div>
                                                <p class="text-sm text-gray-500">Last updated</p>
                                            </div>
                                            <div class="text-right text-sm whitespace-nowrap text-gray-500">
                                                <time datetime="{{ $task->updated_at->format('Y-m-d') }}">{{ $task->updated_at->format('M d, Y') }}</time>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
            
            <div class="px-4 py-4 sm:px-6 bg-gray-50 border-t border-gray-200">
                <div class="flex justify-between">
                    <a href="{{ route('tasks.index') }}" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                        Back to Tasks
                    </a>
                    <div class="flex space-x-2">
                        <a href="{{ route('tasks.edit', $task) }}" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                            Edit Task
                        </a>
                        <form action="{{ route('tasks.destroy', $task) }}" method="POST" class="inline" onsubmit="return confirm('Are you sure you want to delete this task?');">
                            @csrf
                            @method('DELETE')
                            <button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
                                Delete
                            </button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection
                

“resources/views/tasks/edit.blade.php”:

                  @extends('layouts.app')

@section('content')
    <div class="max-w-2xl mx-auto">
        <div class="md:flex md:items-center md:justify-between mb-6">
            <div class="flex-1 min-w-0">
                <h1 class="text-2xl font-bold text-gray-900">Edit Task</h1>
                <p class="mt-1 text-sm text-gray-500">Update task details</p>
            </div>
            <div class="mt-4 md:mt-0 md:ml-4">
                <a href="{{ route('tasks.show', $task) }}" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                    <svg class="-ml-1 mr-2 h-5 w-5 text-gray-500" 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="M11 17l-5-5m0 0l5-5m-5 5h12" />
                    </svg>
                    Back to Task
                </a>
            </div>
        </div>

        <div class="bg-white shadow overflow-hidden sm:rounded-lg">
            <form action="{{ route('tasks.update', $task) }}" method="POST">
                @csrf
                @method('PUT')
                
                <div class="px-4 py-5 sm:p-6">
                    <div class="grid grid-cols-6 gap-6">
                        <div class="col-span-6">
                            <label for="title" class="block text-sm font-medium text-gray-700">Task Title</label>
                            <input type="text" name="title" id="title" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md @error('title') border-red-300 text-red-900 placeholder-red-300 @enderror" value="{{ old('title', $task->title) }}" required>
                            @error('title')
                                <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
                            @enderror
                        </div>

                        <div class="col-span-6">
                            <label for="description" class="block text-sm font-medium text-gray-700">Description</label>
                            <textarea name="description" id="description" rows="4" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md @error('description') border-red-300 text-red-900 placeholder-red-300 @enderror">{{ old('description', $task->description) }}</textarea>
                            @error('description')
                                <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
                            @enderror
                        </div>

                        <div class="col-span-6 sm:col-span-3">
                            <label for="status" class="block text-sm font-medium text-gray-700">Status</label>
                            <select id="status" name="status" class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
                                <option value="pending" {{ old('status', $task->status) == 'pending' ? 'selected' : '' }}>Pending</option>
                                <option value="in_progress" {{ old('status', $task->status) == 'in_progress' ? 'selected' : '' }}>In Progress</option>
                                <option value="completed" {{ old('status', $task->status) == 'completed' ? 'selected' : '' }}>Completed</option>
                            </select>
                            @error('status')
                                <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
                            @enderror
                        </div>

                        <div class="col-span-6 sm:col-span-3">
                            <label for="due_date" class="block text-sm font-medium text-gray-700">Due Date</label>
                            <input type="date" name="due_date" id="due_date" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md" value="{{ old('due_date', $task->due_date ? $task->due_date->format('Y-m-d') : '') }}">
                            @error('due_date')
                                <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
                            @enderror
                        </div>
                    </div>
                </div>
                
                <div class="px-4 py-3 bg-gray-50 text-right sm:px-6 flex justify-between">
                    <a href="{{ route('tasks.show', $task) }}" class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
                        Cancel
                    </a>
                    <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                        Update Task
                    </button>
                </div>
            </form>
        </div>
    </div>
@endsection
                

Để đảm bảo mấy cái thiết kế của chúng ta nó hoạt động trơn tru thì bây giờ chúng ta sẽ đi sửa mấy cái configuration file nhé :v Cố lên, sắp xong rồi

Trong file “vite.config.js” các bạn sửa như sau:

Article image
                  import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});
                

Sau đó theo đường dẫn “resources/css/app.css” và sửa như sau:

                  @tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom classes for our application */
@layer components {
    /* Status badges */
    .status-badge {
        @apply px-2 py-1 text-xs font-medium rounded-full;
    }
    
    .status-pending {
        @apply bg-yellow-100 text-yellow-800 border border-yellow-200;
    }
    
    .status-in_progress {
        @apply bg-blue-100 text-blue-800 border border-blue-200;
    }
    
    .status-completed {
        @apply bg-green-100 text-green-800 border border-green-200;
    }
    
    /* Buttons */
    .btn {
        @apply inline-flex items-center px-4 py-2 border rounded-md font-medium text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors;
    }
    
    .btn-primary {
        @apply btn border-transparent text-white bg-blue-600 hover:bg-blue-700 focus:ring-blue-500;
    }
    
    .btn-secondary {
        @apply btn border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-gray-500;
    }
    
    .btn-danger {
        @apply btn border-transparent text-white bg-red-600 hover:bg-red-700 focus:ring-red-500;
    }
    
    /* Cards */
    .card {
        @apply bg-white rounded-lg shadow-sm overflow-hidden border border-gray-200;
    }
    
    .card-header {
        @apply px-4 py-5 border-b border-gray-200 sm:px-6;
    }
    
    .card-body {
        @apply px-4 py-5 sm:p-6;
    }
    
    .card-footer {
        @apply px-4 py-4 sm:px-6 bg-gray-50 border-t border-gray-200;
    }
    
    /* Form elements */
    .form-label {
        @apply block text-sm font-medium text-gray-700;
    }
    
    .form-input {
        @apply mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md;
    }
    
    .form-error {
        @apply mt-2 text-sm text-red-600;
    }
    
    /* Task card animations */
    .task-card {
        @apply transition-all duration-300 hover:shadow-md;
    }
}

/* Additional styling for clipping long text */
.line-clamp-2 {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
                

Vào tiếp đường dẫn “resources/js/app.js” rồi sửa code như sau:

                  /**
 * Task Manager Main JavaScript
 */

// Close alert messages when the close button is clicked
document.addEventListener('DOMContentLoaded', function() {
    // Handle alert dismissals
    const alerts = document.querySelectorAll('[role="alert"]');
    alerts.forEach(alert => {
        const dismissButton = alert.querySelector('button');
        if (dismissButton) {
            dismissButton.addEventListener('click', () => {
                alert.style.display = 'none';
            });
            
            // Auto-dismiss after 5 seconds
            setTimeout(() => {
                alert.style.opacity = '0';
                alert.style.transition = 'opacity 1s';
                setTimeout(() => {
                    alert.style.display = 'none';
                }, 1000);
            }, 5000);
        }
    });
    
    // Task status filter functionality (optional enhancement)
    const statusFilter = document.querySelector('select[class*="status-filter"]');
    if (statusFilter) {
        statusFilter.addEventListener('change', function() {
            const status = this.value;
            const taskCards = document.querySelectorAll('.task-card');
            
            taskCards.forEach(card => {
                const taskStatus = card.dataset.status;
                if (status === '' || status === taskStatus) {
                    card.style.display = 'block';
                } else {
                    card.style.display = 'none';
                }
            });
        });
    }
    
    // Task search functionality (optional enhancement)
    const searchInput = document.querySelector('input[type="search"]');
    if (searchInput) {
        searchInput.addEventListener('input', function() {
            const searchTerm = this.value.toLowerCase();
            const taskCards = document.querySelectorAll('.task-card');
            
            taskCards.forEach(card => {
                const taskTitle = card.querySelector('h3').textContent.toLowerCase();
                const taskDescription = card.querySelector('p') ? 
                    card.querySelector('p').textContent.toLowerCase() : '';
                
                if (taskTitle.includes(searchTerm) || taskDescription.includes(searchTerm)) {
                    card.style.display = 'block';
                } else {
                    card.style.display = 'none';
                }
            });
        });
    }
});
                

sau đó sửa file “package.json” như sau:

                  {
    "private": true,
    "type": "module",
    "scripts": {
        "dev": "vite",
        "build": "vite build"
    },
    "devDependencies": {
        "@tailwindcss/forms": "^0.5.3",
        "autoprefixer": "^10.4.14",
        "axios": "^1.1.2",
        "laravel-vite-plugin": "^0.7.5",
        "postcss": "^8.4.23",
        "tailwindcss": "^3.3.2",
        "vite": "^4.0.0"
    }
}
                

Và cuối cùng chúng ta cập nhật app.blade.php:

                  @extends('layouts.app')

@section('content')
    <div class="mb-6 flex justify-between items-center">
        <h1 class="text-2xl font-bold text-gray-900">My Tasks</h1>
        <a href="{{ route('tasks.create') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 active:bg-blue-800 focus:outline-none focus:border-blue-800 focus:ring ring-blue-300 disabled:opacity-25 transition">
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" 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>
            New Task
        </a>
    </div>

    <!-- Task Filter Controls (Optional) -->
    <div class="bg-white rounded-lg shadow-sm p-4 mb-6">
        <div class="flex flex-wrap items-center gap-4">
            <div class="flex items-center">
                <span class="text-sm font-medium text-gray-700 mr-2">Filter by Status:</span>
                <select class="status-filter rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
                    <option value="">All Tasks</option>
                    <option value="pending">Pending</option>
                    <option value="in_progress">In Progress</option>
                    <option value="completed">Completed</option>
                </select>
            </div>
            <div class="relative flex-grow max-w-sm">
                <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
                    <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
                    </svg>
                </div>
                <input type="search" class="pl-10 rounded-md border-gray-300 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50 w-full" placeholder="Search tasks...">
            </div>
        </div>
    </div>

    @if($tasks->isEmpty())
        <div class="bg-white rounded-lg shadow-sm p-6 text-center">
            <svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path>
            </svg>
            <h3 class="text-lg font-medium text-gray-900 mb-2">No tasks yet</h3>
            <p class="text-gray-500 mb-4">Get started by creating your first task</p>
            <a href="{{ route('tasks.create') }}" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 active:bg-blue-800 focus:outline-none focus:border-blue-800 focus:ring ring-blue-300 disabled:opacity-25 transition">
                Create Task
            </a>
        </div>
    @else
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            @foreach($tasks as $task)
                <div class="bg-white rounded-lg shadow-sm overflow-hidden border border-gray-200 hover:shadow-md transition-shadow task-card" data-status="{{ $task->status }}">
                    <div class="px-4 py-5 sm:p-6">
                        <div class="flex justify-between items-start">
                            <h3 class="text-lg font-medium text-gray-900 truncate mb-1">{{ $task->title }}</h3>
                            <span class="px-2 py-1 text-xs font-medium rounded-full {{ $task->status == 'pending' ? 'bg-yellow-100 text-yellow-800' : ($task->status == 'in_progress' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800') }}">
                                {{ ucfirst(str_replace('_', ' ', $task->status)) }}
                            </span>
                        </div>
                        
                        @if($task->description)
                            <p class="mt-2 text-sm text-gray-500 line-clamp-2">{{ $task->description }}</p>
                        @endif
                        
                        <div class="mt-4 flex items-center text-sm text-gray-500">
                            <svg class="flex-shrink-0 mr-1.5 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
                            </svg>
                            <span>{{ $task->due_date ? $task->due_date->format('M d, Y') : 'No due date' }}</span>
                        </div>
                    </div>
                    
                    <div class="bg-gray-50 px-4 py-4 sm:px-6 border-t border-gray-200">
                        <div class="flex justify-between items-center">
                            <div class="text-xs text-gray-500">
                                Created {{ $task->created_at->diffForHumans() }}
                            </div>
                            <div class="flex space-x-2">
                                <a href="{{ route('tasks.show', $task) }}" class="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                                    View
                                </a>
                                <a href="{{ route('tasks.edit', $task) }}" class="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
                                    Edit
                                </a>
                            </div>
                        </div>
                    </div>
                </div>
            @endforeach
        </div>
    @endif
@endsection
                

Vậy là đã chúng ta đã hoàn thành một web application rồi nha mọi người 😀

Bây giờ các bạn có thể reset Laragon rồi chạy lệnh “npm run dev”, và tận hưởng thành quả thôi

Article imageArticle image

Mình phát hiện một lỗi là cái status filter hiện tại nó không hoạt động được thì các bạn có thể sửa như sau

Chỉ việc add thêm đoạn code này vào index.blade.php:

                  <!-- Add this at the end of index.blade.php, just before the @endsection -->
<script>
    document.addEventListener('DOMContentLoaded', function() {
        const statusFilter = document.querySelector('.status-filter');
        if (statusFilter) {
            statusFilter.addEventListener('change', function() {
                const status = this.value;
                const taskCards = document.querySelectorAll('.task-card');
                
                taskCards.forEach(card => {
                    if (status === '' || card.dataset.status === status) {
                        card.style.display = 'block';
                    } else {
                        card.style.display = 'none';
                    }
                });
            });
        }
    });
</script>
                

Source:  Published Notion page