feat: implement tour listing with sorting and pagination
This commit is contained in:
@@ -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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
@@ -50,6 +52,26 @@ class Tour extends Model
|
|||||||
'file_id',
|
'file_id',
|
||||||
'tour_type'
|
'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()
|
public function flight()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Pagination\Paginator;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@@ -19,6 +20,6 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
Paginator::useBootstrapFive();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Tour;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
if (!function_exists('generateSortRouteParameter')) {
|
if (!function_exists('generateSortRouteParameter')) {
|
||||||
function generateSortRouteParameter(string $sortKey): array
|
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';
|
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
@@ -27,7 +27,10 @@
|
|||||||
"App\\": "app/",
|
"App\\": "app/",
|
||||||
"Database\\Factories\\": "database/factories/",
|
"Database\\Factories\\": "database/factories/",
|
||||||
"Database\\Seeders\\": "database/seeders/"
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
}
|
},
|
||||||
|
"files": [
|
||||||
|
"app/helpers/helper.php"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
<!-- Be present above all else. - Naval Ravikant -->
|
||||||
|
</div>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\FlightController;
|
use App\Http\Controllers\FlightController;
|
||||||
use App\Http\Controllers\IndexController;
|
use App\Http\Controllers\IndexController;
|
||||||
|
use App\Http\Controllers\TourController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/' , [IndexController::class , 'index'])->name('index');
|
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('/' , 'index')->name('index');
|
||||||
Route::get('{flight}','show')->name('show');
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user