This commit is contained in:
Doar Mihai
2026-04-03 01:04:19 +03:00
parent 2b930a0636
commit 1cbe5bfe3b
12 changed files with 536 additions and 2 deletions
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\JobCategoryResource;
use App\Models\JobCategory;
use Illuminate\Http\Request;
class JobCategoryController extends Controller
{
/**
* Display a listing of job categories
*/
public function index()
{
$categories = JobCategory::withCount('jobs')
->orderBy('name')
->get();
return JobCategoryResource::collection($categories);
}
/**
* Display the specified category
*/
public function show(string $slug)
{
$category = JobCategory::where('slug', $slug)
->withCount('jobs')
->firstOrFail();
return new JobCategoryResource($category);
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\JobCollection;
use App\Http\Resources\JobResource;
use App\Models\Job;
use App\Services\JobSearchService;
use Illuminate\Http\Request;
class JobController extends Controller
{
public function __construct(
private JobSearchService $searchService
) {}
/**
* Display a listing of jobs with filtering and search
*/
public function index(Request $request)
{
$filters = $request->only([
'keywords',
'search',
'location',
'category_id',
'job_type',
'experience_level',
'remote',
'salary_min',
'salary_max',
'posted_within',
'source_id',
'company',
'sort',
]);
$perPage = min($request->input('per_page', 15), 100);
$jobs = $this->searchService->search($filters, $perPage);
return new JobCollection($jobs);
}
/**
* Display the specified job
*/
public function show(int $id)
{
$job = Job::with(['source', 'category', 'jobLocation'])
->findOrFail($id);
return new JobResource($job);
}
/**
* Get autocomplete suggestions for job titles
*/
public function titleSuggestions(Request $request)
{
$query = $request->input('q', '');
if (strlen($query) < 2) {
return response()->json([]);
}
$suggestions = $this->searchService->getTitleSuggestions($query, 10);
return response()->json($suggestions);
}
/**
* Get autocomplete suggestions for companies
*/
public function companySuggestions(Request $request)
{
$query = $request->input('q', '');
if (strlen($query) < 2) {
return response()->json([]);
}
$suggestions = $this->searchService->getCompanySuggestions($query, 10);
return response()->json($suggestions);
}
/**
* Get autocomplete suggestions for locations
*/
public function locationSuggestions(Request $request)
{
$query = $request->input('q', '');
if (strlen($query) < 2) {
return response()->json([]);
}
$suggestions = $this->searchService->getLocationSuggestions($query, 10);
return response()->json($suggestions);
}
/**
* Get filter options
*/
public function filterOptions()
{
return response()->json($this->searchService->getFilterOptions());
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\JobCategory;
use App\Repositories\JobRepository;
use Illuminate\Http\Request;
class HomeController extends Controller
{
public function __construct(
private JobRepository $jobRepository
) {}
/**
* Display the homepage with featured jobs
*/
public function index()
{
$featuredJobs = $this->jobRepository->getFeaturedJobs(10);
$recentJobs = $this->jobRepository->getRecentJobs(7, 6);
$stats = $this->jobRepository->getStatistics();
$categories = JobCategory::withCount('jobs')
->orderBy('name')
->get();
return view('home', compact('featuredJobs', 'recentJobs', 'stats', 'categories'));
}
}
+113
View File
@@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\Job;
use App\Models\JobCategory;
use App\Repositories\JobRepository;
use App\Services\JobSearchService;
use Illuminate\Http\Request;
class JobController extends Controller
{
public function __construct(
private JobSearchService $searchService,
private JobRepository $jobRepository
) {}
/**
* Display a listing of jobs with search and filters
*/
public function index(Request $request)
{
$filters = $request->only([
'keywords',
'location',
'category_id',
'job_type',
'experience_level',
'remote',
'salary_min',
'salary_max',
'posted_within',
'company',
'sort',
]);
$perPage = $request->input('per_page', 20);
$jobs = $this->searchService->search($filters, $perPage);
$categories = JobCategory::withCount('jobs')->orderBy('name')->get();
$filterOptions = $this->searchService->getFilterOptions();
return view('jobs.index', compact('jobs', 'categories', 'filterOptions', 'filters'));
}
/**
* Display the specified job
*/
public function show(int $id)
{
$job = Job::with(['source', 'category', 'jobLocation'])
->findOrFail($id);
$similarJobs = $this->jobRepository->getSimilarJobs($job, 5);
return view('jobs.show', compact('job', 'similarJobs'));
}
/**
* Display jobs by category
*/
public function category(string $slug, Request $request)
{
$category = JobCategory::where('slug', $slug)->firstOrFail();
$filters = $request->only([
'keywords',
'location',
'job_type',
'experience_level',
'remote',
'salary_min',
'salary_max',
'posted_within',
'sort',
]);
$filters['category_id'] = $category->id;
$perPage = $request->input('per_page', 20);
$jobs = $this->searchService->search($filters, $perPage);
$filterOptions = $this->searchService->getFilterOptions();
return view('jobs.category', compact('category', 'jobs', 'filterOptions', 'filters'));
}
/**
* Display remote jobs
*/
public function remote(Request $request)
{
$filters = $request->only([
'keywords',
'category_id',
'job_type',
'experience_level',
'salary_min',
'salary_max',
'posted_within',
'sort',
]);
$filters['remote'] = true;
$perPage = $request->input('per_page', 20);
$jobs = $this->searchService->search($filters, $perPage);
$categories = JobCategory::withCount('jobs')->orderBy('name')->get();
$filterOptions = $this->searchService->getFilterOptions();
return view('jobs.remote', compact('jobs', 'categories', 'filterOptions', 'filters'));
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class JobCategoryResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'description' => $this->description,
'icon' => $this->icon,
'jobs_count' => $this->when(isset($this->jobs_count), $this->jobs_count),
];
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class JobCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'current_page' => $this->currentPage(),
'last_page' => $this->lastPage(),
'per_page' => $this->perPage(),
'from' => $this->firstItem(),
'to' => $this->lastItem(),
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class JobLocationResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'city' => $this->city,
'state' => $this->state,
'country' => $this->country,
'country_code' => $this->country_code,
'formatted' => $this->formatted(),
];
}
/**
* Format location as a readable string
*/
private function formatted(): string
{
$parts = array_filter([
$this->city,
$this->state,
$this->country,
]);
return implode(', ', $parts);
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class JobResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'external_id' => $this->external_id,
'title' => $this->title,
'company' => $this->company,
'description' => $this->description,
'location' => $this->location,
'salary' => [
'min' => $this->salary_min,
'max' => $this->salary_max,
'currency' => $this->salary_currency,
'formatted' => $this->formatSalary(),
],
'job_type' => $this->job_type,
'remote_allowed' => $this->remote_allowed,
'experience_level' => $this->experience_level,
'source' => new JobSourceResource($this->whenLoaded('source')),
'category' => new JobCategoryResource($this->whenLoaded('category')),
'job_location' => new JobLocationResource($this->whenLoaded('jobLocation')),
'urls' => [
'source' => $this->source_url,
'apply' => $this->apply_url,
],
'dates' => [
'posted_at' => $this->posted_at?->toIso8601String(),
'expires_at' => $this->expires_at?->toIso8601String(),
'created_at' => $this->created_at->toIso8601String(),
'updated_at' => $this->updated_at->toIso8601String(),
],
];
}
/**
* Format salary range for display
*/
private function formatSalary(): ?string
{
if (!$this->salary_min && !$this->salary_max) {
return null;
}
$currency = $this->salary_currency ?? 'USD';
$symbol = $currency === 'USD' ? '$' : $currency . ' ';
if ($this->salary_min && $this->salary_max) {
return $symbol . number_format($this->salary_min, 0) . ' - ' . $symbol . number_format($this->salary_max, 0);
}
if ($this->salary_min) {
return $symbol . number_format($this->salary_min, 0) . '+';
}
return 'Up to ' . $symbol . number_format($this->salary_max, 0);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class JobSourceResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'api_type' => $this->api_type,
'website_url' => $this->website_url,
'is_active' => $this->is_active,
];
}
}
+1
View File
@@ -8,6 +8,7 @@ use Illuminate\Console\Scheduling\Schedule;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
+36
View File
@@ -0,0 +1,36 @@
<?php
use App\Http\Controllers\Api\JobCategoryController;
use App\Http\Controllers\Api\JobController;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
// Job endpoints
Route::prefix('jobs')->group(function () {
Route::get('/', [JobController::class, 'index'])->name('api.jobs.index');
Route::get('/{id}', [JobController::class, 'show'])->name('api.jobs.show');
// Autocomplete endpoints
Route::get('/autocomplete/titles', [JobController::class, 'titleSuggestions'])->name('api.jobs.titles');
Route::get('/autocomplete/companies', [JobController::class, 'companySuggestions'])->name('api.jobs.companies');
Route::get('/autocomplete/locations', [JobController::class, 'locationSuggestions'])->name('api.jobs.locations');
// Filter options
Route::get('/filters/options', [JobController::class, 'filterOptions'])->name('api.jobs.filter-options');
});
// Category endpoints
Route::prefix('categories')->group(function () {
Route::get('/', [JobCategoryController::class, 'index'])->name('api.categories.index');
Route::get('/{slug}', [JobCategoryController::class, 'show'])->name('api.categories.show');
});
+11 -2
View File
@@ -1,7 +1,16 @@
<?php
use App\Http\Controllers\Web\HomeController;
use App\Http\Controllers\Web\JobController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
// Homepage
Route::get('/', [HomeController::class, 'index'])->name('home');
// Job routes
Route::prefix('jobs')->group(function () {
Route::get('/', [JobController::class, 'index'])->name('jobs.index');
Route::get('/remote', [JobController::class, 'remote'])->name('jobs.remote');
Route::get('/category/{slug}', [JobController::class, 'category'])->name('jobs.category');
Route::get('/{id}', [JobController::class, 'show'])->name('jobs.show');
});