feat: implement tour listing with sorting and pagination

This commit is contained in:
2026-08-17 16:37:11 +03:30
parent 5350cd1742
commit 9dc27863e2
8 changed files with 392 additions and 2 deletions
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers;
use App\Models\Tour;
use Illuminate\Http\Request;
class TourController extends Controller
{
public function index(Request $request)
{
$title = 'تورها';
$sort = $request->query('sort', 'default');
$tours = Tour::with([
'originCity',
'city',
'tourDepartures',
])
->applySort($sort)
->paginate(3)
->withQueryString();
return view('tours.index', compact('title', 'tours', 'sort'));
}
public function show(Tour $tour)
{
$title= 'جزئیات تور';
return view('tours.show', compact('title'));
}
}
+22
View File
@@ -7,6 +7,8 @@
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
@@ -50,6 +52,26 @@ class Tour extends Model
'file_id',
'tour_type'
];
#[Scope]
protected function applySort(Builder $query, ?string $sort = null): void
{
switch ($sort) {
case 'lowest':
$query->withMin('tourDepartures', 'price')
->orderBy('tour_departures_min_price');
break;
case 'highest':
$query->withMax('tourDepartures', 'price')
->orderByDesc('tour_departures_max_price');
break;
default:
$query->orderByDesc('id');
break;
}
}
public function flight()
{
+2 -1
View File
@@ -2,6 +2,7 @@
namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -19,6 +20,6 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
Paginator::useBootstrapFive();
}
}
+36
View File
@@ -1,5 +1,8 @@
<?php
use App\Models\Tour;
use Carbon\Carbon;
if (!function_exists('generateSortRouteParameter')) {
function generateSortRouteParameter(string $sortKey): array
{
@@ -21,3 +24,36 @@ if (!function_exists('activeSort')) {
return 'text-gray-600 dark:text-gray-300 hover:text-blue-500';
}
}
if (!function_exists('tourDuration')) {
function tourDuration(Tour $tour): string
{
$departure = $tour->tourDepartures->first();
if (!$departure) {
return 'نامشخص';
}
$start = Carbon::parse($departure->start_date);
$end = Carbon::parse($departure->end_date);
$nights = $start->diffInDays($end);
$days = $nights + 1;
return "{$days} روز و {$nights} شب";
}
}
if (!function_exists('tourDateRange')) {
function tourDateRange(Tour $tour): string
{
$departure = $tour->tourDepartures->first();
if (!$departure) {
return 'تاریخ نامشخص';
}
$start = Verta::instance($departure->start_date);
$end = Verta::instance($departure->end_date);
return $start->format('Y/m/d') . ' - ' . $end->format('Y/m/d');
}
}
+4 -1
View File
@@ -27,7 +27,10 @@
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"files": [
"app/helpers/helper.php"
]
},
"autoload-dev": {
"psr-4": {
+285
View File
@@ -0,0 +1,285 @@
@extends('layouts.app')
@section('content')
<main class="main">
<div class="site-breadcrumb" style="background: url(assets/img/breadcrumb/02.jpg)">
<div class="container">
<h2 class="breadcrumb-title">تور با عرض کامل</h2>
<ul class="breadcrumb-menu">
<li><a href="{{route('index')}}">صفحه اصلی</a></li>
<li class="active">تور با عرض کامل</li>
</ul>
</div>
</div>
<div class="search-area">
<div class="container">
<div class="search-wrapper">
<div class="search-box tour-search">
<div class="search-form">
<form action="#">
<div class="tour-search-wrapper">
<div class="row">
<div class="col-lg-4">
<div class="form-group">
<label>مقصد</label>
<div class="form-group-icon">
<input type="text" name="destination" class="form-control"
value="نیو یورک ,آمریکا">
<i class="fal fa-earth-americas"></i>
</div>
<p>کجا می روی؟</p>
</div>
</div>
<div class="col-lg-4">
<div class="form-group">
<div class="search-form-date">
<div class="search-form-journey">
<label>تاریخ سفر</label>
<div class="form-group-icon">
<input type="text" name="journey-date"
class="form-control date-picker journey-date">
<i class="fal fa-calendar-days"></i>
</div>
<p class="journey-day-name"></p>
</div>
<div class="search-form-return">
<label>زمان بازگشت</label>
<div class="form-group-icon">
<input type="text" name="return-date"
class="form-control date-picker return-date">
</div>
<p class="return-day-name"></p>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="form-group dropdown passenger-box">
<div class="passenger-class" role="menu" data-bs-toggle="dropdown"
aria-expanded="false">
<label>اتاق ها، مهمانان</label>
<div class="form-group-icon">
<div class="passenger-total">
<span class="passenger-total-room">2</span> اتاق,
<span class="passenger-total-amount">2</span> میهمان
</div>
<i class="fal fa-user-plus"></i>
</div>
<p class="passenger-class-name">اتاق دو نفره</p>
</div>
<div class="dropdown-menu dropdown-menu-end">
<div class="dropdown-item">
<div class="passenger-item">
<div class="passenger-info">
<h6>بزرگسالان</h6>
<p>+12 سال</p>
</div>
<div class="passenger-qty">
<button type="button" class="minus-btn"><i
class="far fa-minus"></i></button>
<input type="text" name="adult"
class="qty-amount passenger-adult" value="2"
readonly>
<button type="button" class="plus-btn"><i
class="far fa-plus"></i></button>
</div>
</div>
</div>
<div class="dropdown-item">
<div class="passenger-item">
<div class="passenger-info">
<h6>کودکان</h6>
<p>2 تا 12 سال</p>
</div>
<div class="passenger-qty">
<button type="button" class="minus-btn"><i
class="far fa-minus"></i></button>
<input type="text" name="children"
class="qty-amount passenger-children" value="0"
readonly>
<button type="button" class="plus-btn"><i
class="far fa-plus"></i></button>
</div>
</div>
</div>
<div class="dropdown-item">
<div class="passenger-item">
<div class="passenger-info">
<h6>نوزاد</h6>
<p>کمتر از 2 سال</p>
</div>
<div class="passenger-qty">
<button type="button" class="minus-btn"><i
class="far fa-minus"></i></button>
<input type="text" name="infant"
class="qty-amount passenger-infant" value="0"
readonly>
<button type="button" class="plus-btn"><i
class="far fa-plus"></i></button>
</div>
</div>
</div>
<div class="dropdown-item">
<div class="passenger-item">
<div class="passenger-info">
<h6>اتاق ها</h6>
</div>
<div class="passenger-qty">
<button type="button" class="minus-btn"><i
class="far fa-minus"></i></button>
<input type="text" name="room"
class="qty-amount passenger-room" value="2"
readonly>
<button type="button" class="plus-btn"><i
class="far fa-plus"></i></button>
</div>
</div>
</div>
<div class="dropdown-item">
<h6 class="mb-3 mt-2">نوع اتاق</h6>
<div class="passenger-class-info">
<div class="form-check">
<input class="form-check-input" type="radio"
value="Single Room" name="room-type"
id="room-type10">
<label class="form-check-label" for="room-type10">
اتاق تک نفره
</label>
</div>
<div class="form-check">
<input class="form-check-input" checked type="radio"
value="Double Room" name="room-type"
id="room-type11">
<label class="form-check-label" for="room-type11">
اتاق دو نفره
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio"
value="Deluxe Room" name="room-type"
id="room-type12">
<label class="form-check-label" for="room-type12">
اتاق لوکس
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="search-btn">
<button type="submit" class="theme-btn"><span class="far fa-search"></span>جستجو
کردن
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="tour-grid py-120">
<div class="container">
<div class="row">
<div class="col-lg-12 col-xl-12">
<div class="col-md-12">
<div class="booking-sort">
<h5>{{ $tours->total() }} نتیجه پیدا شد</h5>
<div class="col-md-3 booking-sort-box">
<form method="GET"
action="{{ route('tours.index') }}"
class="tour-sort-form">
<select name="sort"
class="form-select"
onchange="this.form.submit()">
<option value="default"
{{ request('sort') == 'default' ? 'selected' : '' }}>
جدیدترین
</option>
<option value="lowest"
{{ request('sort') == 'lowest' ? 'selected' : '' }}>
ارزان‌ترین
</option>
<option value="highest"
{{ request('sort') == 'highest' ? 'selected' : '' }}>
گران‌ترین
</option>
</select>
</form>
</div>
</div>
</div>
<div class="row">
@forelse($tours as $tour)
<div class="col-md-6 col-lg-4">
<div class="tour-item">
<div class="tour-img">
<img src="assets/img/tour/01.jpg" alt>
<a href="#" class="add-wishlist"><i class="far fa-heart"></i></a>
</div>
<div class="tour-content">
<div class="tour-top">
<a href="#" class="tour-category"><i
class="far fa-tag"></i> {{$tour->tour_type}}</a>
</div>
<h4 class="tour-title"><a href="#">{{$tour->title}}</a></h4>
<div class="tour-duration"><i class="far fa-clock"></i>
سفر {{ tourDuration($tour) }}</div>
<div class="tour-date">{{ tourDateRange($tour) }}</div>
<div class="tour-bottom">
<div class="tour-price">
<span>{{number_format($tour->tourDepartures->first()->price)}} تومان</span>
</div>
<div class="tour-text-btn">
<a href="{{route('tours.show' , $tour->id)}}">جزئیات تور <i
class="fas fa-arrow-left"></i></a>
</div>
</div>
</div>
</div>
</div>
@empty
<div class="alert alert-warning">هیچ توری یافت نشد</div>
@endforelse
@if($tours->hasPages())
<div class="pagination-area">
<div aria-label="Page navigation example">
{{ $tours->links() }}
</div>
<div class="pagination-showing">
<p>
نمایش {{ $tours->firstItem() }} -
{{ $tours->lastItem() }}
از {{ $tours->total() }} تور
</p>
</div>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
</main>
@endsection
+3
View File
@@ -0,0 +1,3 @@
<div>
<!-- Be present above all else. - Naval Ravikant -->
</div>
+7
View File
@@ -2,6 +2,7 @@
use App\Http\Controllers\FlightController;
use App\Http\Controllers\IndexController;
use App\Http\Controllers\TourController;
use Illuminate\Support\Facades\Route;
Route::get('/' , [IndexController::class , 'index'])->name('index');
@@ -10,3 +11,9 @@ Route::prefix('flights')->controller(FlightController::class)->name('flights.')-
Route::get('/' , 'index')->name('index');
Route::get('{flight}','show')->name('show');
});
Route::prefix('tours')->controller(TourController::class)->name('tours.')->group(function (){
Route::get('/' , 'index')->name('index');
Route::get('{tour}','show')->name('show');
});