This commit is contained in:
Doar Mihai
2026-04-04 18:19:34 +03:00
parent 5fb17f717d
commit 7c09846bd6
22 changed files with 1229 additions and 94 deletions
+354 -42
View File
@@ -2,6 +2,34 @@
A comprehensive job aggregator platform built with Laravel 13 that fetches, stores, and displays job listings from multiple free job APIs.
## Quick Start
```bash
# Clone repository
git clone <repository-url> && cd jobifygarden
# Install dependencies
composer install
# Start Docker environment (includes MySQL, Redis)
./vendor/bin/sail up -d
# Setup application
./vendor/bin/sail artisan key:generate
./vendor/bin/sail artisan migrate
./vendor/bin/sail artisan db:seed
# Install & build frontend assets
./vendor/bin/sail npm install && ./vendor/bin/sail npm run dev
# Start queue worker (in a separate terminal)
./vendor/bin/sail artisan queue:work
# Visit http://localhost
```
**Important:** After setup, configure the cron job (see [Cron Jobs](#cron-jobs-required) section) to enable automatic job fetching.
## Requirements
- **PHP**: 8.3.x or higher
@@ -40,7 +68,81 @@ A comprehensive job aggregator platform built with Laravel 13 that fetches, stor
## Installation
### Development Setup
### Development Setup (Laravel Sail - Recommended)
1. **Clone the repository**
```bash
git clone <repository-url>
cd jobifygarden
```
2. **Install PHP dependencies**
```bash
composer install
```
3. **Copy environment file**
```bash
cp .env.example .env
```
4. **Configure environment variables**
Edit `.env` and update the following:
```env
APP_NAME=JobifyGarden
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost
# Database (Sail uses these defaults)
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=jobifygarden
DB_USERNAME=sail
DB_PASSWORD=password
# Cache & Sessions (Redis)
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
# Redis (Sail default)
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
```
5. **Start Docker containers**
```bash
./vendor/bin/sail up -d
```
6. **Generate application key**
```bash
./vendor/bin/sail artisan key:generate
```
7. **Run database migrations**
```bash
./vendor/bin/sail artisan migrate
```
8. **Seed initial data (optional)**
```bash
./vendor/bin/sail artisan db:seed
```
9. **Install frontend dependencies and build assets**
```bash
./vendor/bin/sail npm install
./vendor/bin/sail npm run dev
```
10. **Visit the application**
Open `http://localhost` in your browser.
### Production Setup (Traditional Server)
1. **Clone the repository**
```bash
@@ -50,8 +152,9 @@ cd jobifygarden
2. **Install dependencies**
```bash
composer install
composer install --optimize-autoloader --no-dev
npm install
npm run build
```
3. **Configure environment**
@@ -60,72 +163,141 @@ cp .env.example .env
php artisan key:generate
```
4. **Update `.env` with your database credentials**
4. **Update `.env` for production**
```env
APP_NAME=JobifyGarden
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=jobifygarden
DB_USERNAME=your_username
DB_PASSWORD=your_password
DB_PASSWORD=your_secure_password
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=your_redis_password
REDIS_PORT=6379
```
5. **Run migrations**
```bash
php artisan migrate
php artisan migrate --force
```
6. **Seed the database (optional)**
6. **Optimize for production**
```bash
php artisan db:seed
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize
```
7. **Build frontend assets**
7. **Set proper permissions**
```bash
npm run dev
```
8. **Start the development server**
```bash
php artisan serve
```
Visit `http://localhost:8000` to view the application.
### Docker Setup (Laravel Sail)
```bash
# Install dependencies
composer install
# Start Docker containers
./vendor/bin/sail up -d
# Run migrations
./vendor/bin/sail artisan migrate
# Build assets
./vendor/bin/sail npm install
./vendor/bin/sail npm run dev
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
```
## Production Deployment
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed production deployment instructions.
## Required Services
### Queue Worker (Required)
The application uses queues for background job processing. You must run a queue worker:
**Using Laravel Sail (Development):**
```bash
./vendor/bin/sail artisan queue:work --sleep=3 --tries=3 --timeout=90
```
**Using Supervisor (Production):**
Create `/etc/supervisor/conf.d/jobifygarden-worker.conf`:
```ini
[program:jobifygarden-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/jobifygarden/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/path/to/jobifygarden/storage/logs/worker.log
stopwaitsecs=3600
```
Then reload supervisor:
```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start jobifygarden-worker:*
```
### Cron Jobs (Required)
Laravel's task scheduler must run every minute. Add this to your crontab:
**Edit crontab:**
```bash
crontab -e
```
**Add this line:**
```bash
* * * * * cd /path/to/jobifygarden && php artisan schedule:run >> /dev/null 2>&1
```
**For Laravel Sail (Development):**
```bash
* * * * * cd /path/to/jobifygarden && ./vendor/bin/sail artisan schedule:run >> /dev/null 2>&1
```
### Scheduled Tasks
The following tasks are automatically scheduled and will run via the cron job above:
| Task | Schedule | Description |
|------|----------|-------------|
| `jobs:fetch` | Every 2 hours | Fetch new jobs from all API sources |
| `jobs:clean` | Daily at 3:00 AM | Remove expired and old jobs |
| `jobs:deduplicate` | Daily at 4:00 AM | Remove duplicate job listings |
| `cache:prune-stale-tags` | Hourly | Clean up stale cache tags |
**Note:** Job fetching intervals can be customized in `app/Console/Kernel.php`
## Available Commands
### Job Management
```bash
# Fetch jobs from all sources
# Fetch jobs from all sources (automatically scheduled)
php artisan jobs:fetch
# Clean expired jobs
# Fetch from a specific source
php artisan jobs:fetch --source=indeed
# Clean expired jobs (automatically scheduled)
php artisan jobs:clean
# Remove duplicate jobs
# Clean jobs older than specific days
php artisan jobs:clean --days=60
# Remove duplicate jobs (automatically scheduled)
php artisan jobs:deduplicate
# Force dedupe even with different sources
php artisan jobs:deduplicate --force
# Enrich job data (categories, experience levels)
php artisan jobs:enrich
@@ -133,24 +305,55 @@ php artisan jobs:enrich
php artisan jobs:test-apis
```
### Database Optimization
### Database Management
```bash
# Optimize database tables and rebuild indexes
# Run migrations
php artisan migrate
# Rollback last migration
php artisan migrate:rollback
# Fresh migration with seeding
php artisan migrate:fresh --seed
# Optimize database tables
php artisan db:optimize
```
### Cache Management
```bash
# Clear all caches
php artisan optimize:clear
# Clear specific caches
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
# Build caches for production
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize
```
### Queue Management
```bash
# Start queue worker
php artisan queue:work
# Process jobs with options
php artisan queue:work --queue=high,default --sleep=3 --tries=3
# List failed jobs
php artisan queue:failed
# Retry all failed jobs
php artisan queue:retry all
# Retry specific failed job
php artisan queue:retry {id}
# Clear all failed jobs
php artisan queue:flush
```
## Project Structure
@@ -194,8 +397,9 @@ GET /api/jobs/autocomplete/companies # Company autocomplete
### Backend
- **Laravel 13**: PHP framework
- **PHP 8.3+**: Programming language
- **MySQL 8**: Primary database
- **Redis**: Caching and queues
- **Redis**: Caching, sessions, and queues
- **Guzzle**: HTTP client for API requests
### Frontend
@@ -204,6 +408,114 @@ GET /api/jobs/autocomplete/companies # Company autocomplete
- **TailwindCSS**: Utility-first CSS framework
- **Vite**: Frontend build tool
### DevOps
- **Laravel Sail**: Docker development environment
- **Supervisor**: Process manager for queue workers
- **Nginx/Apache**: Web server
- **Certbot**: SSL certificate management
## Troubleshooting
### Common Issues
**1. Queue jobs not processing**
```bash
# Check if queue worker is running
ps aux | grep "queue:work"
# Restart queue worker (Supervisor)
sudo supervisorctl restart jobifygarden-worker:*
# Or start manually for debugging
php artisan queue:work --verbose
```
**2. Jobs not being fetched automatically**
```bash
# Verify cron is set up
crontab -l
# Test schedule manually
php artisan schedule:run
# Check schedule list
php artisan schedule:list
```
**3. Redis connection errors**
```bash
# Check Redis is running
redis-cli ping
# For Sail
./vendor/bin/sail redis redis-cli ping
# Restart Redis (Sail)
./vendor/bin/sail down
./vendor/bin/sail up -d
```
**4. Permission errors**
```bash
# Fix storage permissions
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
# For development (use your user)
sudo chown -R $USER:$USER storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
```
**5. Slugs not generating for new jobs**
```bash
# Regenerate slugs for existing jobs
php artisan tinker
# Then run:
# \App\Models\Job::whereNull('slug')->chunk(100, function($jobs) {
# foreach($jobs as $job) {
# $job->slug = \App\Models\Job::generateUniqueSlug($job->title, $job->company);
# $job->save();
# }
# });
```
**6. Cache issues**
```bash
# Clear all caches
php artisan optimize:clear
# Rebuild for production
php artisan optimize
```
### Performance Tips
1. **Enable OPcache in production** (add to `php.ini`):
```ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
```
2. **Optimize database queries:**
```bash
php artisan db:optimize
```
3. **Use queue workers with multiple processes:**
```bash
# Update supervisor config to use more workers
numprocs=4
```
4. **Monitor slow queries:**
```bash
# Enable query logging in .env
DB_LOG_QUERIES=true
```
## License
This project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
+14 -1
View File
@@ -25,6 +25,19 @@ class HomeController extends Controller
->orderBy('name')
->get();
return view('home', compact('featuredJobs', 'recentJobs', 'stats', 'categories'));
// Get filter options for search form
$jobTypes = \App\Models\Job::select('job_type')
->whereNotNull('job_type')
->distinct()
->pluck('job_type')
->toArray();
$experienceLevels = \App\Models\Job::select('experience_level')
->whereNotNull('experience_level')
->distinct()
->pluck('experience_level')
->toArray();
return view('home', compact('featuredJobs', 'recentJobs', 'stats', 'categories', 'jobTypes', 'experienceLevels'));
}
}
+3 -3
View File
@@ -53,10 +53,10 @@ class JobController extends Controller
/**
* Display the specified job
*/
public function show(int $id)
public function show(Job $job)
{
$job = Job::with(['source', 'category', 'jobLocation'])
->findOrFail($id);
// Load relationships
$job->load(['source', 'category', 'jobLocation']);
// Track job view event
AnalyticsEvent::logEvent(AnalyticsEvent::EVENT_JOB_VIEW, $job->id);
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use Illuminate\View\View;
class PageController extends Controller
{
public function about(): View
{
return view('pages.about');
}
public function contact(): View
{
return view('pages.contact');
}
public function privacy(): View
{
return view('pages.privacy');
}
public function terms(): View
{
return view('pages.terms');
}
}
+55
View File
@@ -6,13 +6,32 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class Job extends Model
{
use SoftDeletes;
protected static function boot()
{
parent::boot();
static::creating(function ($job) {
if (empty($job->slug)) {
$job->slug = static::generateUniqueSlug($job->title, $job->company);
}
});
static::updating(function ($job) {
if ($job->isDirty('title') && empty($job->slug)) {
$job->slug = static::generateUniqueSlug($job->title, $job->company);
}
});
}
protected $fillable = [
'external_id',
'slug',
'title',
'company',
'description',
@@ -187,4 +206,40 @@ class Job extends Model
{
return $query->where('company', 'LIKE', "%{$company}%");
}
/**
* Generate a unique slug for the job.
*/
public static function generateUniqueSlug(string $title, ?string $company = null): string
{
// Create base slug from title
$baseSlug = Str::slug($title);
// Add company to slug if provided for better uniqueness
if ($company) {
$baseSlug .= '-' . Str::slug(Str::limit($company, 30, ''));
}
// Limit slug length to avoid database issues
$baseSlug = Str::limit($baseSlug, 200, '');
$slug = $baseSlug;
$counter = 1;
// Check if slug exists and append counter if needed
while (static::withTrashed()->where('slug', $slug)->exists()) {
$slug = $baseSlug . '-' . $counter;
$counter++;
}
return $slug;
}
/**
* Get the route key for the model.
*/
public function getRouteKeyName(): string
{
return 'slug';
}
}
@@ -46,9 +46,16 @@ class AdzunaTransformer implements JobTransformer
private function sanitizeDescription(string $description): string
{
$clean = strip_tags($description);
// Adzuna returns truncated descriptions with ellipsis (...)
// Keep the description as-is but strip excessive HTML while preserving basic formatting
$clean = strip_tags($description, '<p><br><ul><ol><li><strong><b><em><i><h1><h2><h3><h4>');
$clean = html_entity_decode($clean, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$clean = preg_replace('/\s+/', ' ', $clean);
// Convert multiple spaces to single space but preserve newlines
$clean = preg_replace('/[^\S\n]+/', ' ', $clean);
// Note: Adzuna API only provides truncated descriptions in search results.
// Full descriptions would require individual job detail API calls.
return trim($clean);
}
@@ -70,9 +70,32 @@ class RemotiveTransformer implements JobTransformer
return null;
}
// Try to extract numbers from salary string
if (preg_match_all('/[\d,]+/', $salary, $matches)) {
$numbers = array_map(fn($n) => (float) str_replace(',', '', $n), $matches[0]);
// Normalize the salary string
$salary = strtolower($salary);
// Check if it's hourly rate - if so, convert to annual (assuming 40hr/week * 52 weeks = 2080 hours/year)
$isHourly = str_contains($salary, '/hour') || str_contains($salary, 'per hour') || str_contains($salary, '/hr');
// Try to extract numbers with optional k/K suffix
// Pattern: number followed by optional comma-separated digits, then optional 'k'
if (preg_match_all('/\$?\s*([\d,]+)\s*([kK])?/', $salary, $matches, PREG_SET_ORDER)) {
$numbers = [];
foreach ($matches as $match) {
$number = (float) str_replace(',', '', $match[1]);
// If there's a 'k' suffix, multiply by 1000
if (!empty($match[2]) && strtolower($match[2]) === 'k') {
$number *= 1000;
}
// If it's hourly, convert to annual
if ($isHourly) {
$number *= 2080; // 40 hours/week * 52 weeks
}
$numbers[] = $number;
}
if (count($numbers) >= 2) {
return $type === 'min' ? min($numbers) : max($numbers);
+1 -1
View File
@@ -15,7 +15,7 @@ class SeoService
{
$title = $job->title . ' at ' . $job->company . ' - JobifyGarden';
$description = Str::limit(strip_tags($job->description), 155);
$url = route('jobs.show', $job->id);
$url = route('jobs.show', $job);
return [
'title' => $title,
+4 -4
View File
@@ -43,12 +43,12 @@ class SitemapService
})
->orderBy('posted_at', 'desc')
->limit(10000) // Limit to 10k most recent jobs
->get(['id', 'posted_at', 'updated_at']);
->get(['id', 'slug', 'posted_at', 'updated_at']);
foreach ($jobs as $job) {
$this->addUrl(
$xml,
route('jobs.show', $job->id),
route('jobs.show', $job),
$job->updated_at ?? $job->posted_at,
'weekly',
'0.7'
@@ -150,12 +150,12 @@ class SitemapService
})
->orderBy('posted_at', 'desc')
->limit(50000) // Sitemap limit
->get(['id', 'posted_at', 'updated_at']);
->get(['id', 'slug', 'posted_at', 'updated_at']);
foreach ($jobs as $job) {
$this->addUrl(
$xml,
route('jobs.show', $job->id),
route('jobs.show', $job),
$job->updated_at ?? $job->posted_at,
'weekly',
'0.7'
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('jobs', function (Blueprint $table) {
$table->string('slug')->unique()->nullable()->after('id');
$table->index('slug');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('jobs', function (Blueprint $table) {
$table->dropIndex(['slug']);
$table->dropColumn('slug');
});
}
};
@@ -4,7 +4,7 @@
<div class="flex justify-between items-start mb-3">
<div class="flex-1">
<h3 class="text-lg font-semibold text-gray-900 mb-1">
<a href="{{ route('jobs.show', $job->id) }}" class="hover:text-emerald-600">
<a href="{{ route('jobs.show', $job) }}" class="hover:text-emerald-600">
{{ $job->title }}
</a>
</h3>
@@ -71,7 +71,7 @@
<span class="text-xs text-gray-500">
Posted {{ $job->posted_at ? $job->posted_at->diffForHumans() : $job->created_at->diffForHumans() }}
</span>
<a href="{{ route('jobs.show', $job->id) }}"
<a href="{{ route('jobs.show', $job) }}"
class="inline-flex items-center px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-medium rounded-md transition-colors duration-200">
View Details
<svg class="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+100 -12
View File
@@ -37,35 +37,123 @@
<!-- Hero Section -->
<div class="bg-gradient-to-r from-emerald-600 to-emerald-500">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
<div class="text-center">
<div class="text-center mb-10">
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-6">
Find Your Dream Job
</h1>
<p class="text-xl text-emerald-50 mb-8 max-w-2xl mx-auto">
<p class="text-xl text-emerald-50 max-w-2xl mx-auto">
Search through thousands of job listings from multiple sources. Your next career opportunity is just a search away.
</p>
</div>
<!-- Search Bar -->
<div class="max-w-3xl mx-auto">
<form action="{{ route('jobs.index') }}" method="GET" class="flex flex-col sm:flex-row gap-2">
<div class="flex-1">
<!-- Advanced Search Form -->
<div class="max-w-5xl mx-auto">
<form action="{{ route('jobs.index') }}" method="GET" class="bg-white rounded-xl shadow-2xl p-6 space-y-4">
<!-- Main Search Field -->
<div>
<input type="text"
name="keywords"
placeholder="Job title, keywords, or company"
class="w-full px-4 py-3 rounded-lg border-0 focus:ring-2 focus:ring-emerald-600 text-gray-900 placeholder-gray-500">
placeholder="Job title, keywords, or company..."
class="w-full px-4 py-4 text-lg rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-gray-900 placeholder-gray-500">
</div>
<div class="flex-1">
<!-- Filter Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<!-- Category -->
<div>
<select name="category_id"
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-gray-700 bg-white">
<option value="">All Categories</option>
@foreach($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</select>
</div>
<!-- Location -->
<div>
<input type="text"
name="location"
placeholder="Location"
class="w-full px-4 py-3 rounded-lg border-0 focus:ring-2 focus:ring-emerald-600 text-gray-900 placeholder-gray-500">
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-gray-700 placeholder-gray-500">
</div>
<!-- Job Type -->
<div>
<select name="job_type"
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-gray-700 bg-white">
<option value="">All Job Types</option>
@foreach($jobTypes as $type)
<option value="{{ $type }}">{{ ucfirst(str_replace('_', ' ', $type)) }}</option>
@endforeach
</select>
</div>
<!-- Experience Level -->
<div>
<select name="experience_level"
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-gray-700 bg-white">
<option value="">All Experience Levels</option>
@foreach($experienceLevels as $level)
<option value="{{ $level }}">{{ ucfirst($level) }}</option>
@endforeach
</select>
</div>
</div>
<!-- Additional Filters Row -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<!-- Remote -->
<div>
<select name="remote"
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-gray-700 bg-white">
<option value="">Remote/On-site</option>
<option value="1">Remote Only</option>
<option value="0">On-site Only</option>
</select>
</div>
<!-- Posted Within -->
<div>
<select name="posted_within"
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-gray-700 bg-white">
<option value="">Any Time</option>
<option value="1">Last 24 hours</option>
<option value="7">Last 7 days</option>
<option value="30">Last 30 days</option>
</select>
</div>
<!-- Salary Min -->
<div>
<input type="number"
name="salary_min"
placeholder="Min Salary"
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-gray-700 placeholder-gray-500">
</div>
<!-- Salary Max -->
<div>
<input type="number"
name="salary_max"
placeholder="Max Salary"
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-gray-700 placeholder-gray-500">
</div>
</div>
<!-- Search Button -->
<div class="flex justify-center pt-2">
<button type="submit"
class="px-8 py-3 bg-emerald-800 hover:bg-emerald-900 text-white font-semibold rounded-lg transition-colors duration-200">
class="px-12 py-4 bg-emerald-600 hover:bg-emerald-700 text-white text-lg font-semibold rounded-lg transition-colors duration-200 shadow-lg hover:shadow-xl">
<span class="flex items-center">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
Search Jobs
</span>
</button>
</form>
</div>
</form>
</div>
</div>
</div>
+2 -2
View File
@@ -15,8 +15,8 @@
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "CollectionPage",
"{{ '@' }}context": "https://schema.org",
"{{ '@' }}type": "CollectionPage",
"name": "{{ $category->name }} Jobs",
"description": "{{ 'Browse ' . $jobs->total() . ' ' . $category->name . ' job opportunities' }}",
"url": "{{ route('jobs.category', $category->slug) }}"
+2 -2
View File
@@ -14,8 +14,8 @@
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "CollectionPage",
"{{ '@' }}context": "https://schema.org",
"{{ '@' }}type": "CollectionPage",
"name": "Browse Jobs",
"description": "Browse thousands of job opportunities from multiple sources",
"url": "{{ route('jobs.index') }}"
+2 -2
View File
@@ -15,8 +15,8 @@
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "CollectionPage",
"{{ '@' }}context": "https://schema.org",
"{{ '@' }}type": "CollectionPage",
"name": "Remote Jobs",
"description": "Work from anywhere remote job opportunities",
"url": "{{ route('jobs.remote') }}"
+103 -9
View File
@@ -2,7 +2,7 @@
@section('title', $job->title . ' at ' . $job->company . ' - JobifyGarden')
@section('description', Str::limit(strip_tags($job->description), 155))
@section('canonical', route('jobs.show', $job->id))
@section('canonical', route('jobs.show', $job))
@push('structured_data')
<script type="application/ld+json">
@@ -20,7 +20,7 @@
$breadcrumbsArray[] = ['name' => $job->category->name, 'url' => route('jobs.category', $job->category->slug)];
}
$breadcrumbsArray[] = ['name' => $job->title, 'url' => route('jobs.show', $job->id)];
$breadcrumbsArray[] = ['name' => $job->title, 'url' => route('jobs.show', $job)];
@endphp
{!! json_encode(app(\App\Services\SeoService::class)->generateBreadcrumbStructuredData($breadcrumbsArray), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
@@ -111,10 +111,24 @@
</div>
<!-- Job Description -->
<div class="prose prose-emerald max-w-none mb-8">
<div class="mb-8">
<h3 class="text-xl font-semibold text-gray-900 mb-4">Job Description</h3>
<div class="text-gray-700 leading-relaxed">
{!! nl2br(e($job->description)) !!}
<div class="prose prose-emerald prose-lg max-w-none text-gray-700">
{!! Str::of($job->description)
->pipe(function($desc) {
// Allow safe HTML tags
return strip_tags($desc, '<p><br><ul><ol><li><strong><b><em><i><h1><h2><h3><h4><h5><h6><a><div><span>');
})
->pipe(function($desc) {
// Convert remaining newlines to paragraphs if no HTML structure
if (!str_contains($desc, '<p>') && !str_contains($desc, '<div>')) {
$paragraphs = array_filter(explode("\n\n", $desc));
return '<p>' . implode('</p><p>', array_map(fn($p) => nl2br(trim($p)), $paragraphs)) . '</p>';
}
return $desc;
})
->toString()
!!}
</div>
</div>
@@ -159,12 +173,12 @@
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 class="text-sm font-semibold text-gray-900 mb-3">Share this job</h3>
<div class="flex gap-2">
<a href="https://twitter.com/intent/tweet?text={{ urlencode($job->title . ' at ' . $job->company) }}&url={{ urlencode(route('jobs.show', $job->id)) }}"
<a href="https://twitter.com/intent/tweet?text={{ urlencode($job->title . ' at ' . $job->company) }}&url={{ urlencode(route('jobs.show', $job)) }}"
target="_blank"
class="flex-1 px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white text-center rounded text-sm">
Twitter
</a>
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{ urlencode(route('jobs.show', $job->id)) }}"
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{ urlencode(route('jobs.show', $job)) }}"
target="_blank"
class="flex-1 px-3 py-2 bg-blue-700 hover:bg-blue-800 text-white text-center rounded text-sm">
LinkedIn
@@ -173,10 +187,90 @@
</div>
<!-- Report -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<button class="text-sm text-red-600 hover:text-red-700">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6" x-data="{ showReportModal: false }">
<button @click="showReportModal = true" class="text-sm text-red-600 hover:text-red-700">
Report this job
</button>
<!-- Report Modal -->
<div x-show="showReportModal"
x-cloak
@click.away="showReportModal = false"
@keydown.escape.window="showReportModal = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;">
<!-- Overlay -->
<div class="fixed inset-0 bg-black bg-opacity-50"></div>
<!-- Modal -->
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Report this job</h3>
<button @click="showReportModal = false" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<p class="text-sm text-gray-600 mb-4">
Please select a reason for reporting this job posting:
</p>
<form action="#" method="POST" class="space-y-4">
@csrf
<input type="hidden" name="job_id" value="{{ $job->id }}">
<div class="space-y-2">
<label class="flex items-center">
<input type="radio" name="reason" value="expired" class="mr-2" required>
<span class="text-sm">Job posting has expired</span>
</label>
<label class="flex items-center">
<input type="radio" name="reason" value="scam" class="mr-2" required>
<span class="text-sm">Suspected scam or fraud</span>
</label>
<label class="flex items-center">
<input type="radio" name="reason" value="duplicate" class="mr-2" required>
<span class="text-sm">Duplicate posting</span>
</label>
<label class="flex items-center">
<input type="radio" name="reason" value="inappropriate" class="mr-2" required>
<span class="text-sm">Inappropriate content</span>
</label>
<label class="flex items-center">
<input type="radio" name="reason" value="other" class="mr-2" required>
<span class="text-sm">Other</span>
</label>
</div>
<div>
<label for="details" class="block text-sm font-medium text-gray-700 mb-1">
Additional details (optional)
</label>
<textarea id="details" name="details" rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
placeholder="Provide more information..."></textarea>
</div>
<div class="flex gap-3">
<button type="button"
@click="showReportModal = false"
class="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50">
Cancel
</button>
<button type="submit"
class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg">
Submit Report
</button>
</div>
<p class="text-xs text-gray-500 text-center">
Thank you for helping us maintain quality job listings.
</p>
</form>
</div>
</div>
</div>
</div>
</div>
+9 -4
View File
@@ -36,6 +36,11 @@
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
<!-- Alpine.js x-cloak CSS -->
<style>
[x-cloak] { display: none !important; }
</style>
@stack('head')
</head>
<body class="h-full antialiased bg-gray-50">
@@ -145,10 +150,10 @@
<div>
<h4 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Resources</h4>
<ul class="space-y-2">
<li><a href="#" class="text-gray-600 hover:text-emerald-600 text-sm">About Us</a></li>
<li><a href="#" class="text-gray-600 hover:text-emerald-600 text-sm">Contact</a></li>
<li><a href="#" class="text-gray-600 hover:text-emerald-600 text-sm">Privacy Policy</a></li>
<li><a href="#" class="text-gray-600 hover:text-emerald-600 text-sm">Terms of Service</a></li>
<li><a href="{{ route('about') }}" class="text-gray-600 hover:text-emerald-600 text-sm">About Us</a></li>
<li><a href="{{ route('contact') }}" class="text-gray-600 hover:text-emerald-600 text-sm">Contact</a></li>
<li><a href="{{ route('privacy') }}" class="text-gray-600 hover:text-emerald-600 text-sm">Privacy Policy</a></li>
<li><a href="{{ route('terms') }}" class="text-gray-600 hover:text-emerald-600 text-sm">Terms of Service</a></li>
</ul>
</div>
</div>
+76
View File
@@ -0,0 +1,76 @@
@extends('layouts.app')
@section('title', 'About Us - JobifyGarden')
@section('description', 'Learn about JobifyGarden, your trusted job aggregator platform connecting job seekers with opportunities from multiple sources.')
@section('canonical', route('about'))
@section('content')
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Header -->
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-gray-900 mb-4">About JobifyGarden</h1>
<p class="text-xl text-gray-600">Your trusted job aggregator platform</p>
</div>
<!-- Content -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 space-y-8">
<!-- Mission Section -->
<section>
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Our Mission</h2>
<p class="text-gray-700 leading-relaxed">
JobifyGarden was created with a simple mission: to make job searching easier and more efficient.
We aggregate job listings from multiple trusted sources, bringing thousands of opportunities to
one convenient platform. Whether you're looking for remote work, full-time positions, or freelance
opportunities, we're here to help you find your next career move.
</p>
</section>
<!-- What We Do Section -->
<section>
<h2 class="text-2xl font-semibold text-gray-900 mb-4">What We Do</h2>
<p class="text-gray-700 leading-relaxed mb-4">
JobifyGarden aggregates job listings from various free job APIs and job boards, providing you with:
</p>
<ul class="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>Access to thousands of job listings from multiple sources</li>
<li>Advanced search and filtering capabilities</li>
<li>Real-time job updates from trusted platforms</li>
<li>Remote and on-site job opportunities across various industries</li>
<li>Easy-to-use interface for efficient job hunting</li>
</ul>
</section>
<!-- Why Choose Us Section -->
<section>
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Why Choose JobifyGarden</h2>
<div class="grid md:grid-cols-2 gap-6">
<div class="bg-emerald-50 p-6 rounded-lg">
<h3 class="font-semibold text-gray-900 mb-2">Comprehensive Coverage</h3>
<p class="text-gray-700 text-sm">We aggregate jobs from multiple sources, so you don't have to visit multiple websites.</p>
</div>
<div class="bg-emerald-50 p-6 rounded-lg">
<h3 class="font-semibold text-gray-900 mb-2">Always Up-to-Date</h3>
<p class="text-gray-700 text-sm">Our automated system regularly fetches new job listings to keep our database current.</p>
</div>
<div class="bg-emerald-50 p-6 rounded-lg">
<h3 class="font-semibold text-gray-900 mb-2">Smart Search</h3>
<p class="text-gray-700 text-sm">Advanced filtering options help you find exactly what you're looking for quickly.</p>
</div>
<div class="bg-emerald-50 p-6 rounded-lg">
<h3 class="font-semibold text-gray-900 mb-2">Free to Use</h3>
<p class="text-gray-700 text-sm">All features are completely free. No hidden fees or premium subscriptions required.</p>
</div>
</div>
</section>
<!-- Contact CTA -->
<section class="bg-gray-50 p-6 rounded-lg text-center">
<h2 class="text-xl font-semibold text-gray-900 mb-3">Have Questions?</h2>
<p class="text-gray-700 mb-4">We'd love to hear from you.</p>
<a href="{{ route('contact') }}" class="inline-block px-6 py-3 bg-emerald-600 hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors duration-200">
Contact Us
</a>
</section>
</div>
</div>
@endsection
+122
View File
@@ -0,0 +1,122 @@
@extends('layouts.app')
@section('title', 'Contact Us - JobifyGarden')
@section('description', 'Get in touch with JobifyGarden. We\'re here to help with any questions or feedback you may have.')
@section('canonical', route('contact'))
@section('content')
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Header -->
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-gray-900 mb-4">Contact Us</h1>
<p class="text-xl text-gray-600">We'd love to hear from you</p>
</div>
<div class="grid md:grid-cols-2 gap-8">
<!-- Contact Information -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Get In Touch</h2>
<div class="space-y-6">
<!-- Email -->
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">Email</h3>
<p class="text-gray-600">hello@jobifygarden.com</p>
</div>
</div>
<!-- Support -->
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">Support</h3>
<p class="text-gray-600">support@jobifygarden.com</p>
</div>
</div>
<!-- Business Hours -->
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="ml-4">
<h3 class="text-lg font-medium text-gray-900">Response Time</h3>
<p class="text-gray-600">We typically respond within 24-48 hours</p>
</div>
</div>
</div>
<!-- FAQ Link -->
<div class="mt-8 pt-8 border-t border-gray-200">
<h3 class="text-lg font-medium text-gray-900 mb-2">Frequently Asked Questions</h3>
<p class="text-gray-600 text-sm mb-4">
Before reaching out, you might find answers to common questions in our FAQ section.
</p>
</div>
</div>
<!-- Contact Form -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Send Us a Message</h2>
<form class="space-y-6">
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">Name</label>
<input type="text" id="name" name="name"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
placeholder="Your name">
</div>
<!-- Email -->
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
<input type="email" id="email" name="email"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
placeholder="your.email@example.com">
</div>
<!-- Subject -->
<div>
<label for="subject" class="block text-sm font-medium text-gray-700 mb-2">Subject</label>
<input type="text" id="subject" name="subject"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
placeholder="How can we help?">
</div>
<!-- Message -->
<div>
<label for="message" class="block text-sm font-medium text-gray-700 mb-2">Message</label>
<textarea id="message" name="message" rows="5"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
placeholder="Tell us more about your question or feedback..."></textarea>
</div>
<!-- Submit Button -->
<div>
<button type="submit"
class="w-full px-6 py-3 bg-emerald-600 hover:bg-emerald-700 text-white font-semibold rounded-lg transition-colors duration-200">
Send Message
</button>
</div>
<p class="text-xs text-gray-500 text-center">
Note: This is a demo form. For actual inquiries, please email us directly at hello@jobifygarden.com
</p>
</form>
</div>
</div>
</div>
@endsection
+120
View File
@@ -0,0 +1,120 @@
@extends('layouts.app')
@section('title', 'Privacy Policy - JobifyGarden')
@section('description', 'Read JobifyGarden\'s privacy policy to understand how we collect, use, and protect your information.')
@section('canonical', route('privacy'))
@section('content')
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Header -->
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-gray-900 mb-4">Privacy Policy</h1>
<p class="text-gray-600">Last updated: {{ now()->format('F d, Y') }}</p>
</div>
<!-- Content -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 prose prose-emerald max-w-none">
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Introduction</h2>
<p class="text-gray-700 leading-relaxed">
JobifyGarden ("we", "our", or "us") is committed to protecting your privacy. This Privacy Policy
explains how we collect, use, disclose, and safeguard your information when you visit our website.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Information We Collect</h2>
<h3 class="text-xl font-semibold text-gray-900 mb-3 mt-6">Usage Data</h3>
<p class="text-gray-700 leading-relaxed mb-4">
We may collect information that your browser sends whenever you visit our website. This may include:
</p>
<ul class="list-disc list-inside space-y-2 text-gray-700 ml-4 mb-4">
<li>Your computer's Internet Protocol (IP) address</li>
<li>Browser type and version</li>
<li>Pages you visit on our site</li>
<li>Time and date of your visit</li>
<li>Time spent on pages</li>
<li>Other diagnostic data</li>
</ul>
<h3 class="text-xl font-semibold text-gray-900 mb-3 mt-6">Cookies and Tracking Technologies</h3>
<p class="text-gray-700 leading-relaxed">
We use cookies and similar tracking technologies to track activity on our website and store certain
information. You can instruct your browser to refuse all cookies or to indicate when a cookie is
being sent.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">How We Use Your Information</h2>
<p class="text-gray-700 leading-relaxed mb-4">
We may use the information we collect for various purposes, including:
</p>
<ul class="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>To provide and maintain our service</li>
<li>To improve and personalize user experience</li>
<li>To analyze how our service is used</li>
<li>To monitor the usage of our service</li>
<li>To detect, prevent and address technical issues</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Third-Party Services</h2>
<p class="text-gray-700 leading-relaxed">
JobifyGarden aggregates job listings from various third-party sources. When you click on a job
listing to apply, you will be redirected to the original job posting on a third-party website.
We are not responsible for the privacy practices of these third-party websites.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Data Security</h2>
<p class="text-gray-700 leading-relaxed">
The security of your data is important to us, but remember that no method of transmission over the
Internet or method of electronic storage is 100% secure. While we strive to use commercially
acceptable means to protect your information, we cannot guarantee its absolute security.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Links to Other Sites</h2>
<p class="text-gray-700 leading-relaxed">
Our service contains links to other sites (job postings from various sources). If you click on a
third-party link, you will be directed to that site. We strongly advise you to review the Privacy
Policy of every site you visit. We have no control over and assume no responsibility for the content,
privacy policies, or practices of any third-party sites or services.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Children's Privacy</h2>
<p class="text-gray-700 leading-relaxed">
Our service is not intended for use by children under the age of 18. We do not knowingly collect
personally identifiable information from children under 18. If you are a parent or guardian and you
are aware that your child has provided us with personal data, please contact us.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Changes to This Privacy Policy</h2>
<p class="text-gray-700 leading-relaxed">
We may update our Privacy Policy from time to time. We will notify you of any changes by posting the
new Privacy Policy on this page and updating the "Last updated" date at the top of this Privacy Policy.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Contact Us</h2>
<p class="text-gray-700 leading-relaxed mb-4">
If you have any questions about this Privacy Policy, please contact us:
</p>
<ul class="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>By email: privacy@jobifygarden.com</li>
<li>By visiting our <a href="{{ route('contact') }}" class="text-emerald-600 hover:text-emerald-700">contact page</a></li>
</ul>
</section>
</div>
</div>
@endsection
+154
View File
@@ -0,0 +1,154 @@
@extends('layouts.app')
@section('title', 'Terms of Service - JobifyGarden')
@section('description', 'Read JobifyGarden\'s terms of service to understand the rules and regulations for using our platform.')
@section('canonical', route('terms'))
@section('content')
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Header -->
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-gray-900 mb-4">Terms of Service</h1>
<p class="text-gray-600">Last updated: {{ now()->format('F d, Y') }}</p>
</div>
<!-- Content -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8 prose prose-emerald max-w-none">
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Agreement to Terms</h2>
<p class="text-gray-700 leading-relaxed">
By accessing or using JobifyGarden, you agree to be bound by these Terms of Service and all
applicable laws and regulations. If you do not agree with any of these terms, you are prohibited
from using or accessing this site.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Use License</h2>
<p class="text-gray-700 leading-relaxed mb-4">
Permission is granted to temporarily access the materials (information or software) on JobifyGarden
for personal, non-commercial transitory viewing only. This is the grant of a license, not a transfer
of title, and under this license you may not:
</p>
<ul class="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>Modify or copy the materials</li>
<li>Use the materials for any commercial purpose or for any public display</li>
<li>Attempt to reverse engineer any software contained on JobifyGarden</li>
<li>Remove any copyright or other proprietary notations from the materials</li>
<li>Transfer the materials to another person or "mirror" the materials on any other server</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Disclaimer</h2>
<p class="text-gray-700 leading-relaxed mb-4">
JobifyGarden is a job aggregation platform that collects and displays job listings from various
third-party sources. We make no warranties or representations about:
</p>
<ul class="list-disc list-inside space-y-2 text-gray-700 ml-4 mb-4">
<li>The accuracy, completeness, or reliability of job listings</li>
<li>The availability of job positions</li>
<li>The legitimacy of employers or job postings</li>
<li>The outcome of any job application</li>
</ul>
<p class="text-gray-700 leading-relaxed">
The materials on JobifyGarden are provided on an 'as is' basis. JobifyGarden makes no warranties,
expressed or implied, and hereby disclaims and negates all other warranties including, without
limitation, implied warranties or conditions of merchantability, fitness for a particular purpose,
or non-infringement of intellectual property or other violation of rights.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Third-Party Content</h2>
<p class="text-gray-700 leading-relaxed">
All job listings displayed on JobifyGarden are sourced from third-party job boards and APIs.
JobifyGarden does not create, verify, or endorse these job listings. When you apply for a job,
you will be redirected to the original posting on a third-party website. We are not responsible
for the content, accuracy, or practices of these third-party sites.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">User Conduct</h2>
<p class="text-gray-700 leading-relaxed mb-4">
You agree not to use JobifyGarden to:
</p>
<ul class="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>Violate any applicable laws or regulations</li>
<li>Engage in any automated data collection (scraping, bots, etc.) without permission</li>
<li>Transmit any harmful code, viruses, or malicious software</li>
<li>Impersonate another person or entity</li>
<li>Interfere with or disrupt the service or servers</li>
<li>Attempt to gain unauthorized access to any part of the service</li>
</ul>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Intellectual Property</h2>
<p class="text-gray-700 leading-relaxed">
The JobifyGarden name, logo, and all related names, logos, product and service names, designs, and
slogans are trademarks of JobifyGarden or its affiliates. You must not use such marks without our
prior written permission. All other names, logos, product and service names, designs, and slogans
on this website are the trademarks of their respective owners.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Limitation of Liability</h2>
<p class="text-gray-700 leading-relaxed">
In no event shall JobifyGarden or its suppliers be liable for any damages (including, without
limitation, damages for loss of data or profit, or due to business interruption) arising out of
the use or inability to use JobifyGarden, even if JobifyGarden or a JobifyGarden authorized
representative has been notified orally or in writing of the possibility of such damage.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Indemnification</h2>
<p class="text-gray-700 leading-relaxed">
You agree to indemnify, defend, and hold harmless JobifyGarden and its officers, directors, employees,
and agents from and against any claims, liabilities, damages, losses, and expenses, including reasonable
attorneys' fees, arising out of or in any way connected with your access to or use of the service.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Modifications to Service</h2>
<p class="text-gray-700 leading-relaxed">
JobifyGarden reserves the right to modify or discontinue, temporarily or permanently, the service
(or any part thereof) with or without notice. You agree that JobifyGarden shall not be liable to
you or to any third party for any modification, suspension, or discontinuance of the service.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Governing Law</h2>
<p class="text-gray-700 leading-relaxed">
These terms and conditions are governed by and construed in accordance with the laws of the
United States, and you irrevocably submit to the exclusive jurisdiction of the courts in that location.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Changes to Terms</h2>
<p class="text-gray-700 leading-relaxed">
We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a
revision is material, we will provide at least 30 days' notice prior to any new terms taking effect.
What constitutes a material change will be determined at our sole discretion.
</p>
</section>
<section class="mb-8">
<h2 class="text-2xl font-semibold text-gray-900 mb-4">Contact Information</h2>
<p class="text-gray-700 leading-relaxed mb-4">
If you have any questions about these Terms, please contact us:
</p>
<ul class="list-disc list-inside space-y-2 text-gray-700 ml-4">
<li>By email: legal@jobifygarden.com</li>
<li>By visiting our <a href="{{ route('contact') }}" class="text-emerald-600 hover:text-emerald-700">contact page</a></li>
</ul>
</section>
</div>
</div>
@endsection
+8 -1
View File
@@ -3,18 +3,25 @@
use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\Web\HomeController;
use App\Http\Controllers\Web\JobController;
use App\Http\Controllers\Web\PageController;
use App\Http\Controllers\Web\SitemapController;
use Illuminate\Support\Facades\Route;
// Homepage
Route::get('/', [HomeController::class, 'index'])->name('home');
// Static pages
Route::get('/about', [PageController::class, 'about'])->name('about');
Route::get('/contact', [PageController::class, 'contact'])->name('contact');
Route::get('/privacy', [PageController::class, 'privacy'])->name('privacy');
Route::get('/terms', [PageController::class, 'terms'])->name('terms');
// 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');
Route::get('/{job:slug}', [JobController::class, 'show'])->name('jobs.show');
});
// Sitemap routes