saved jobs

This commit is contained in:
Doar Mihai
2026-05-01 02:58:06 +03:00
parent 75388cfb95
commit 17097c3bb1
7 changed files with 133 additions and 14 deletions
@@ -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'));
}
}
+14 -7
View File
@@ -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));
});
} }
} }
+14 -1
View File
@@ -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">
+36
View File
@@ -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
+21
View File
@@ -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 -->
+9
View File
@@ -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
View File
@@ -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