saved jobs
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Web;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Job;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class BookmarkController extends Controller
|
||||||
|
{
|
||||||
|
public function toggle(Request $request, Job $job)
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$user->savedJobs()->toggle($job->id);
|
||||||
|
|
||||||
|
if ($request->wantsJson()) {
|
||||||
|
$bookmarked = $user->savedJobs()->where('job_id', $job->id)->exists();
|
||||||
|
return response()->json(['bookmarked' => $bookmarked]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$jobs = $request->user()->savedJobs()->latest('user_saved_jobs.created_at')->paginate(20);
|
||||||
|
return view('jobs.bookmarks', compact('jobs'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,23 +2,30 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\View;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Register any application services.
|
|
||||||
*/
|
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Bootstrap any application services.
|
|
||||||
*/
|
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
View::composer('components.job-card', function ($view) {
|
||||||
|
static $bookmarkedIds = null;
|
||||||
|
|
||||||
|
if ($bookmarkedIds === null) {
|
||||||
|
$bookmarkedIds = Auth::check()
|
||||||
|
? Auth::user()->savedJobs()->pluck('job_id')->flip()
|
||||||
|
: collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
$job = $view->getData()['job'];
|
||||||
|
$view->with('bookmarked', $bookmarkedIds->has($job->id));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@props(['job'])
|
@props(['job', 'bookmarked' => false])
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow duration-200">
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow duration-200">
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
@@ -10,11 +10,24 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<p class="text-gray-700 font-medium">{{ $job->company }}</p>
|
<p class="text-gray-700 font-medium">{{ $job->company }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2 ml-3 shrink-0">
|
||||||
@if($job->remote_allowed)
|
@if($job->remote_allowed)
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-800">
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-800">
|
||||||
Remote
|
Remote
|
||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
|
@auth
|
||||||
|
<form method="POST" action="{{ route('jobs.bookmark', $job) }}" x-data="{ saved: {{ $bookmarked ? 'true' : 'false' }} }">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" @click="saved = !saved" title="{{ $bookmarked ? 'Remove bookmark' : 'Bookmark' }}"
|
||||||
|
class="text-gray-400 hover:text-emerald-600 transition-colors duration-150">
|
||||||
|
<svg class="w-5 h-5" :fill="saved ? 'currentColor' : 'none'" :class="saved ? 'text-emerald-600' : ''" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-4.5L5 21V5z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-3 text-sm text-gray-600 mb-4">
|
<div class="flex flex-wrap gap-3 text-sm text-gray-600 mb-4">
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'My Bookmarks - JobifyGarden')
|
||||||
|
@section('description', 'Your saved job listings on JobifyGarden.')
|
||||||
|
@section('canonical', route('bookmarks'))
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">My Bookmarks</h1>
|
||||||
|
<p class="text-gray-600 mt-1">{{ $jobs->total() }} saved {{ Str::plural('job', $jobs->total()) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($jobs->isEmpty())
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
|
||||||
|
<svg class="w-12 h-12 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-4.5L5 21V5z"/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-gray-500 text-lg mb-4">No bookmarks yet.</p>
|
||||||
|
<a href="{{ route('jobs.index') }}" class="inline-flex items-center px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-medium rounded-md">
|
||||||
|
Browse Jobs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
@foreach($jobs as $job)
|
||||||
|
<x-job-card :job="$job" />
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
{{ $jobs->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -181,6 +181,27 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<p class="text-xs text-gray-500 text-center mt-3">You will be redirected to the company's application page</p>
|
<p class="text-xs text-gray-500 text-center mt-3">You will be redirected to the company's application page</p>
|
||||||
|
@auth
|
||||||
|
<form method="POST" action="{{ route('jobs.bookmark', $job) }}" class="mt-3"
|
||||||
|
x-data="{ saved: {{ auth()->user()->savedJobs()->where('job_id', $job->id)->exists() ? 'true' : 'false' }} }">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" @click="saved = !saved"
|
||||||
|
class="flex items-center justify-center w-full px-6 py-3 border rounded-lg font-semibold transition-colors duration-200"
|
||||||
|
:class="saved ? 'border-emerald-600 text-emerald-600 bg-emerald-50 hover:bg-emerald-100' : 'border-gray-300 text-gray-700 hover:border-emerald-400 hover:text-emerald-600'">
|
||||||
|
<svg class="w-5 h-5 mr-2" :fill="saved ? 'currentColor' : 'none'" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-4.5L5 21V5z"/>
|
||||||
|
</svg>
|
||||||
|
<span x-text="saved ? 'Bookmarked' : 'Bookmark'"></span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="flex items-center justify-center w-full mt-3 px-6 py-3 border border-gray-300 text-gray-500 rounded-lg text-sm hover:border-emerald-400 hover:text-emerald-600 transition-colors duration-200">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-4.5L5 21V5z"/>
|
||||||
|
</svg>
|
||||||
|
Log in to bookmark
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Share -->
|
<!-- Share -->
|
||||||
|
|||||||
@@ -86,6 +86,12 @@
|
|||||||
<!-- Auth links (desktop) -->
|
<!-- Auth links (desktop) -->
|
||||||
<div class="hidden sm:flex sm:items-center sm:space-x-4">
|
<div class="hidden sm:flex sm:items-center sm:space-x-4">
|
||||||
@auth
|
@auth
|
||||||
|
<a href="{{ route('bookmarks') }}" class="text-sm font-medium text-gray-500 hover:text-gray-700 flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-4.5L5 21V5z"/>
|
||||||
|
</svg>
|
||||||
|
Bookmarks
|
||||||
|
</a>
|
||||||
<a href="{{ route('admin.dashboard') }}" class="text-sm font-medium text-gray-500 hover:text-gray-700">Dashboard</a>
|
<a href="{{ route('admin.dashboard') }}" class="text-sm font-medium text-gray-500 hover:text-gray-700">Dashboard</a>
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
@csrf
|
@csrf
|
||||||
@@ -136,6 +142,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pt-2 pb-3 border-t border-gray-200 space-y-1">
|
<div class="pt-2 pb-3 border-t border-gray-200 space-y-1">
|
||||||
@auth
|
@auth
|
||||||
|
<a href="{{ route('bookmarks') }}" class="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700">
|
||||||
|
Bookmarks
|
||||||
|
</a>
|
||||||
<a href="{{ route('admin.dashboard') }}" class="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700">
|
<a href="{{ route('admin.dashboard') }}" class="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700">
|
||||||
Dashboard
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
+5
-1
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\Admin\DashboardController;
|
use App\Http\Controllers\Admin\DashboardController;
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
|
use App\Http\Controllers\Web\BookmarkController;
|
||||||
use App\Http\Controllers\Web\HomeController;
|
use App\Http\Controllers\Web\HomeController;
|
||||||
use App\Http\Controllers\Web\JobController;
|
use App\Http\Controllers\Web\JobController;
|
||||||
use App\Http\Controllers\Web\PageController;
|
use App\Http\Controllers\Web\PageController;
|
||||||
@@ -31,11 +32,14 @@ Route::get('/sitemap-index.xml', [SitemapController::class, 'sitemapIndex'])->na
|
|||||||
Route::get('/sitemap-main.xml', [SitemapController::class, 'mainSitemap'])->name('sitemap.main');
|
Route::get('/sitemap-main.xml', [SitemapController::class, 'mainSitemap'])->name('sitemap.main');
|
||||||
Route::get('/sitemap-jobs.xml', [SitemapController::class, 'jobsSitemap'])->name('sitemap.jobs');
|
Route::get('/sitemap-jobs.xml', [SitemapController::class, 'jobsSitemap'])->name('sitemap.jobs');
|
||||||
|
|
||||||
// Profile (Breeze)
|
// Authenticated user routes
|
||||||
Route::middleware('auth')->group(function () {
|
Route::middleware('auth')->group(function () {
|
||||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||||
|
|
||||||
|
Route::get('/bookmarks', [BookmarkController::class, 'index'])->name('bookmarks');
|
||||||
|
Route::post('/jobs/{job}/bookmark', [BookmarkController::class, 'toggle'])->name('jobs.bookmark');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin routes
|
// Admin routes
|
||||||
|
|||||||
Reference in New Issue
Block a user