From 1cbe5bfe3bdf31f557de231266b8026ec1e40496 Mon Sep 17 00:00:00 2001 From: Doar Mihai Date: Fri, 3 Apr 2026 01:04:19 +0300 Subject: [PATCH] phase 5 --- .../Controllers/Api/JobCategoryController.php | 35 ++++++ app/Http/Controllers/Api/JobController.php | 112 +++++++++++++++++ app/Http/Controllers/Web/HomeController.php | 30 +++++ app/Http/Controllers/Web/JobController.php | 113 ++++++++++++++++++ app/Http/Resources/JobCategoryResource.php | 26 ++++ app/Http/Resources/JobCollection.php | 35 ++++++ app/Http/Resources/JobLocationResource.php | 40 +++++++ app/Http/Resources/JobResource.php | 71 +++++++++++ app/Http/Resources/JobSourceResource.php | 26 ++++ bootstrap/app.php | 1 + routes/api.php | 36 ++++++ routes/web.php | 13 +- 12 files changed, 536 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/Api/JobCategoryController.php create mode 100644 app/Http/Controllers/Api/JobController.php create mode 100644 app/Http/Controllers/Web/HomeController.php create mode 100644 app/Http/Controllers/Web/JobController.php create mode 100644 app/Http/Resources/JobCategoryResource.php create mode 100644 app/Http/Resources/JobCollection.php create mode 100644 app/Http/Resources/JobLocationResource.php create mode 100644 app/Http/Resources/JobResource.php create mode 100644 app/Http/Resources/JobSourceResource.php create mode 100644 routes/api.php diff --git a/app/Http/Controllers/Api/JobCategoryController.php b/app/Http/Controllers/Api/JobCategoryController.php new file mode 100644 index 0000000..73ef0bb --- /dev/null +++ b/app/Http/Controllers/Api/JobCategoryController.php @@ -0,0 +1,35 @@ +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); + } +} diff --git a/app/Http/Controllers/Api/JobController.php b/app/Http/Controllers/Api/JobController.php new file mode 100644 index 0000000..ecd6eb9 --- /dev/null +++ b/app/Http/Controllers/Api/JobController.php @@ -0,0 +1,112 @@ +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()); + } +} diff --git a/app/Http/Controllers/Web/HomeController.php b/app/Http/Controllers/Web/HomeController.php new file mode 100644 index 0000000..48f3831 --- /dev/null +++ b/app/Http/Controllers/Web/HomeController.php @@ -0,0 +1,30 @@ +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')); + } +} diff --git a/app/Http/Controllers/Web/JobController.php b/app/Http/Controllers/Web/JobController.php new file mode 100644 index 0000000..7caef6b --- /dev/null +++ b/app/Http/Controllers/Web/JobController.php @@ -0,0 +1,113 @@ +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')); + } +} diff --git a/app/Http/Resources/JobCategoryResource.php b/app/Http/Resources/JobCategoryResource.php new file mode 100644 index 0000000..49473e1 --- /dev/null +++ b/app/Http/Resources/JobCategoryResource.php @@ -0,0 +1,26 @@ + + */ + 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), + ]; + } +} diff --git a/app/Http/Resources/JobCollection.php b/app/Http/Resources/JobCollection.php new file mode 100644 index 0000000..c0cf302 --- /dev/null +++ b/app/Http/Resources/JobCollection.php @@ -0,0 +1,35 @@ + + */ + 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(), + ], + ]; + } +} diff --git a/app/Http/Resources/JobLocationResource.php b/app/Http/Resources/JobLocationResource.php new file mode 100644 index 0000000..f3230d9 --- /dev/null +++ b/app/Http/Resources/JobLocationResource.php @@ -0,0 +1,40 @@ + + */ + 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); + } +} diff --git a/app/Http/Resources/JobResource.php b/app/Http/Resources/JobResource.php new file mode 100644 index 0000000..7df22e6 --- /dev/null +++ b/app/Http/Resources/JobResource.php @@ -0,0 +1,71 @@ + + */ + 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); + } +} diff --git a/app/Http/Resources/JobSourceResource.php b/app/Http/Resources/JobSourceResource.php new file mode 100644 index 0000000..06b6788 --- /dev/null +++ b/app/Http/Resources/JobSourceResource.php @@ -0,0 +1,26 @@ + + */ + 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, + ]; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 232de5e..02e4321 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -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', ) diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..314942c --- /dev/null +++ b/routes/api.php @@ -0,0 +1,36 @@ +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'); +}); diff --git a/routes/web.php b/routes/web.php index 86a06c5..401cf4d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,16 @@ 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'); });