fixes
This commit is contained in:
@@ -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.
|
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
|
## Requirements
|
||||||
|
|
||||||
- **PHP**: 8.3.x or higher
|
- **PHP**: 8.3.x or higher
|
||||||
@@ -40,7 +68,81 @@ A comprehensive job aggregator platform built with Laravel 13 that fetches, stor
|
|||||||
|
|
||||||
## Installation
|
## 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**
|
1. **Clone the repository**
|
||||||
```bash
|
```bash
|
||||||
@@ -50,8 +152,9 @@ cd jobifygarden
|
|||||||
|
|
||||||
2. **Install dependencies**
|
2. **Install dependencies**
|
||||||
```bash
|
```bash
|
||||||
composer install
|
composer install --optimize-autoloader --no-dev
|
||||||
npm install
|
npm install
|
||||||
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Configure environment**
|
3. **Configure environment**
|
||||||
@@ -60,72 +163,141 @@ cp .env.example .env
|
|||||||
php artisan key:generate
|
php artisan key:generate
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Update `.env` with your database credentials**
|
4. **Update `.env` for production**
|
||||||
```env
|
```env
|
||||||
|
APP_NAME=JobifyGarden
|
||||||
|
APP_ENV=production
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_URL=https://yourdomain.com
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=jobifygarden
|
DB_DATABASE=jobifygarden
|
||||||
DB_USERNAME=your_username
|
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**
|
5. **Run migrations**
|
||||||
```bash
|
```bash
|
||||||
php artisan migrate
|
php artisan migrate --force
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Seed the database (optional)**
|
6. **Optimize for production**
|
||||||
```bash
|
```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
|
```bash
|
||||||
npm run dev
|
chown -R www-data:www-data storage bootstrap/cache
|
||||||
```
|
chmod -R 775 storage bootstrap/cache
|
||||||
|
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production Deployment
|
## Production Deployment
|
||||||
|
|
||||||
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed production deployment instructions.
|
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
|
## Available Commands
|
||||||
|
|
||||||
### Job Management
|
### Job Management
|
||||||
```bash
|
```bash
|
||||||
# Fetch jobs from all sources
|
# Fetch jobs from all sources (automatically scheduled)
|
||||||
php artisan jobs:fetch
|
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
|
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
|
php artisan jobs:deduplicate
|
||||||
|
|
||||||
|
# Force dedupe even with different sources
|
||||||
|
php artisan jobs:deduplicate --force
|
||||||
|
|
||||||
# Enrich job data (categories, experience levels)
|
# Enrich job data (categories, experience levels)
|
||||||
php artisan jobs:enrich
|
php artisan jobs:enrich
|
||||||
|
|
||||||
@@ -133,24 +305,55 @@ php artisan jobs:enrich
|
|||||||
php artisan jobs:test-apis
|
php artisan jobs:test-apis
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database Optimization
|
### Database Management
|
||||||
```bash
|
```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
|
php artisan db:optimize
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cache Management
|
### Cache Management
|
||||||
```bash
|
```bash
|
||||||
# Clear all caches
|
# Clear all caches
|
||||||
|
php artisan optimize:clear
|
||||||
|
|
||||||
|
# Clear specific caches
|
||||||
php artisan cache:clear
|
php artisan cache:clear
|
||||||
php artisan config:clear
|
php artisan config:clear
|
||||||
php artisan route:clear
|
php artisan route:clear
|
||||||
php artisan view:clear
|
php artisan view:clear
|
||||||
|
|
||||||
# Build caches for production
|
# Build caches for production
|
||||||
php artisan config:cache
|
php artisan optimize
|
||||||
php artisan route:cache
|
```
|
||||||
php artisan view:cache
|
|
||||||
|
### 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
|
## Project Structure
|
||||||
@@ -194,8 +397,9 @@ GET /api/jobs/autocomplete/companies # Company autocomplete
|
|||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
- **Laravel 13**: PHP framework
|
- **Laravel 13**: PHP framework
|
||||||
|
- **PHP 8.3+**: Programming language
|
||||||
- **MySQL 8**: Primary database
|
- **MySQL 8**: Primary database
|
||||||
- **Redis**: Caching and queues
|
- **Redis**: Caching, sessions, and queues
|
||||||
- **Guzzle**: HTTP client for API requests
|
- **Guzzle**: HTTP client for API requests
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
@@ -204,6 +408,114 @@ GET /api/jobs/autocomplete/companies # Company autocomplete
|
|||||||
- **TailwindCSS**: Utility-first CSS framework
|
- **TailwindCSS**: Utility-first CSS framework
|
||||||
- **Vite**: Frontend build tool
|
- **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
|
## License
|
||||||
|
|
||||||
This project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
This project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ class HomeController extends Controller
|
|||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get();
|
->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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,10 +53,10 @@ class JobController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display the specified job
|
* Display the specified job
|
||||||
*/
|
*/
|
||||||
public function show(int $id)
|
public function show(Job $job)
|
||||||
{
|
{
|
||||||
$job = Job::with(['source', 'category', 'jobLocation'])
|
// Load relationships
|
||||||
->findOrFail($id);
|
$job->load(['source', 'category', 'jobLocation']);
|
||||||
|
|
||||||
// Track job view event
|
// Track job view event
|
||||||
AnalyticsEvent::logEvent(AnalyticsEvent::EVENT_JOB_VIEW, $job->id);
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,32 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class Job extends Model
|
class Job extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
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 = [
|
protected $fillable = [
|
||||||
'external_id',
|
'external_id',
|
||||||
|
'slug',
|
||||||
'title',
|
'title',
|
||||||
'company',
|
'company',
|
||||||
'description',
|
'description',
|
||||||
@@ -187,4 +206,40 @@ class Job extends Model
|
|||||||
{
|
{
|
||||||
return $query->where('company', 'LIKE', "%{$company}%");
|
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
|
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 = 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);
|
return trim($clean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,9 +70,32 @@ class RemotiveTransformer implements JobTransformer
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to extract numbers from salary string
|
// Normalize the salary string
|
||||||
if (preg_match_all('/[\d,]+/', $salary, $matches)) {
|
$salary = strtolower($salary);
|
||||||
$numbers = array_map(fn($n) => (float) str_replace(',', '', $n), $matches[0]);
|
|
||||||
|
// 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) {
|
if (count($numbers) >= 2) {
|
||||||
return $type === 'min' ? min($numbers) : max($numbers);
|
return $type === 'min' ? min($numbers) : max($numbers);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class SeoService
|
|||||||
{
|
{
|
||||||
$title = $job->title . ' at ' . $job->company . ' - JobifyGarden';
|
$title = $job->title . ' at ' . $job->company . ' - JobifyGarden';
|
||||||
$description = Str::limit(strip_tags($job->description), 155);
|
$description = Str::limit(strip_tags($job->description), 155);
|
||||||
$url = route('jobs.show', $job->id);
|
$url = route('jobs.show', $job);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ class SitemapService
|
|||||||
})
|
})
|
||||||
->orderBy('posted_at', 'desc')
|
->orderBy('posted_at', 'desc')
|
||||||
->limit(10000) // Limit to 10k most recent jobs
|
->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) {
|
foreach ($jobs as $job) {
|
||||||
$this->addUrl(
|
$this->addUrl(
|
||||||
$xml,
|
$xml,
|
||||||
route('jobs.show', $job->id),
|
route('jobs.show', $job),
|
||||||
$job->updated_at ?? $job->posted_at,
|
$job->updated_at ?? $job->posted_at,
|
||||||
'weekly',
|
'weekly',
|
||||||
'0.7'
|
'0.7'
|
||||||
@@ -150,12 +150,12 @@ class SitemapService
|
|||||||
})
|
})
|
||||||
->orderBy('posted_at', 'desc')
|
->orderBy('posted_at', 'desc')
|
||||||
->limit(50000) // Sitemap limit
|
->limit(50000) // Sitemap limit
|
||||||
->get(['id', 'posted_at', 'updated_at']);
|
->get(['id', 'slug', 'posted_at', 'updated_at']);
|
||||||
|
|
||||||
foreach ($jobs as $job) {
|
foreach ($jobs as $job) {
|
||||||
$this->addUrl(
|
$this->addUrl(
|
||||||
$xml,
|
$xml,
|
||||||
route('jobs.show', $job->id),
|
route('jobs.show', $job),
|
||||||
$job->updated_at ?? $job->posted_at,
|
$job->updated_at ?? $job->posted_at,
|
||||||
'weekly',
|
'weekly',
|
||||||
'0.7'
|
'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 justify-between items-start mb-3">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-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 }}
|
{{ $job->title }}
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<span class="text-xs text-gray-500">
|
<span class="text-xs text-gray-500">
|
||||||
Posted {{ $job->posted_at ? $job->posted_at->diffForHumans() : $job->created_at->diffForHumans() }}
|
Posted {{ $job->posted_at ? $job->posted_at->diffForHumans() : $job->created_at->diffForHumans() }}
|
||||||
</span>
|
</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">
|
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
|
View Details
|
||||||
<svg class="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
+104
-16
@@ -37,35 +37,123 @@
|
|||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="bg-gradient-to-r from-emerald-600 to-emerald-500">
|
<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="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">
|
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-white mb-6">
|
||||||
Find Your Dream Job
|
Find Your Dream Job
|
||||||
</h1>
|
</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.
|
Search through thousands of job listings from multiple sources. Your next career opportunity is just a search away.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Search Bar -->
|
<!-- Advanced Search Form -->
|
||||||
<div class="max-w-3xl mx-auto">
|
<div class="max-w-5xl mx-auto">
|
||||||
<form action="{{ route('jobs.index') }}" method="GET" class="flex flex-col sm:flex-row gap-2">
|
<form action="{{ route('jobs.index') }}" method="GET" class="bg-white rounded-xl shadow-2xl p-6 space-y-4">
|
||||||
<div class="flex-1">
|
<!-- Main Search Field -->
|
||||||
<input type="text"
|
<div>
|
||||||
name="keywords"
|
<input type="text"
|
||||||
placeholder="Job title, keywords, or company"
|
name="keywords"
|
||||||
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>
|
||||||
|
|
||||||
|
<!-- 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>
|
</div>
|
||||||
<div class="flex-1">
|
|
||||||
|
<!-- Location -->
|
||||||
|
<div>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="location"
|
name="location"
|
||||||
placeholder="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>
|
</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"
|
<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">
|
||||||
Search Jobs
|
<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>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"{{ '@' }}context": "https://schema.org",
|
||||||
"@type": "CollectionPage",
|
"{{ '@' }}type": "CollectionPage",
|
||||||
"name": "{{ $category->name }} Jobs",
|
"name": "{{ $category->name }} Jobs",
|
||||||
"description": "{{ 'Browse ' . $jobs->total() . ' ' . $category->name . ' job opportunities' }}",
|
"description": "{{ 'Browse ' . $jobs->total() . ' ' . $category->name . ' job opportunities' }}",
|
||||||
"url": "{{ route('jobs.category', $category->slug) }}"
|
"url": "{{ route('jobs.category', $category->slug) }}"
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"{{ '@' }}context": "https://schema.org",
|
||||||
"@type": "CollectionPage",
|
"{{ '@' }}type": "CollectionPage",
|
||||||
"name": "Browse Jobs",
|
"name": "Browse Jobs",
|
||||||
"description": "Browse thousands of job opportunities from multiple sources",
|
"description": "Browse thousands of job opportunities from multiple sources",
|
||||||
"url": "{{ route('jobs.index') }}"
|
"url": "{{ route('jobs.index') }}"
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"{{ '@' }}context": "https://schema.org",
|
||||||
"@type": "CollectionPage",
|
"{{ '@' }}type": "CollectionPage",
|
||||||
"name": "Remote Jobs",
|
"name": "Remote Jobs",
|
||||||
"description": "Work from anywhere remote job opportunities",
|
"description": "Work from anywhere remote job opportunities",
|
||||||
"url": "{{ route('jobs.remote') }}"
|
"url": "{{ route('jobs.remote') }}"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
@section('title', $job->title . ' at ' . $job->company . ' - JobifyGarden')
|
@section('title', $job->title . ' at ' . $job->company . ' - JobifyGarden')
|
||||||
@section('description', Str::limit(strip_tags($job->description), 155))
|
@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')
|
@push('structured_data')
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
$breadcrumbsArray[] = ['name' => $job->category->name, 'url' => route('jobs.category', $job->category->slug)];
|
$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
|
@endphp
|
||||||
{!! json_encode(app(\App\Services\SeoService::class)->generateBreadcrumbStructuredData($breadcrumbsArray), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
|
{!! json_encode(app(\App\Services\SeoService::class)->generateBreadcrumbStructuredData($breadcrumbsArray), JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) !!}
|
||||||
@@ -111,10 +111,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Job Description -->
|
<!-- 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>
|
<h3 class="text-xl font-semibold text-gray-900 mb-4">Job Description</h3>
|
||||||
<div class="text-gray-700 leading-relaxed">
|
<div class="prose prose-emerald prose-lg max-w-none text-gray-700">
|
||||||
{!! nl2br(e($job->description)) !!}
|
{!! 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -159,12 +173,12 @@
|
|||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
<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>
|
<h3 class="text-sm font-semibold text-gray-900 mb-3">Share this job</h3>
|
||||||
<div class="flex gap-2">
|
<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"
|
target="_blank"
|
||||||
class="flex-1 px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white text-center rounded text-sm">
|
class="flex-1 px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white text-center rounded text-sm">
|
||||||
Twitter
|
Twitter
|
||||||
</a>
|
</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"
|
target="_blank"
|
||||||
class="flex-1 px-3 py-2 bg-blue-700 hover:bg-blue-800 text-white text-center rounded text-sm">
|
class="flex-1 px-3 py-2 bg-blue-700 hover:bg-blue-800 text-white text-center rounded text-sm">
|
||||||
LinkedIn
|
LinkedIn
|
||||||
@@ -173,10 +187,90 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Report -->
|
<!-- Report -->
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6" x-data="{ showReportModal: false }">
|
||||||
<button class="text-sm text-red-600 hover:text-red-700">
|
<button @click="showReportModal = true" class="text-sm text-red-600 hover:text-red-700">
|
||||||
Report this job
|
Report this job
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,6 +36,11 @@
|
|||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
|
||||||
|
<!-- Alpine.js x-cloak CSS -->
|
||||||
|
<style>
|
||||||
|
[x-cloak] { display: none !important; }
|
||||||
|
</style>
|
||||||
|
|
||||||
@stack('head')
|
@stack('head')
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full antialiased bg-gray-50">
|
<body class="h-full antialiased bg-gray-50">
|
||||||
@@ -145,10 +150,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Resources</h4>
|
<h4 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Resources</h4>
|
||||||
<ul class="space-y-2">
|
<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="{{ route('about') }}" 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="{{ route('contact') }}" 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="{{ route('privacy') }}" 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('terms') }}" class="text-gray-600 hover:text-emerald-600 text-sm">Terms of Service</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -3,18 +3,25 @@
|
|||||||
use App\Http\Controllers\Admin\DashboardController;
|
use App\Http\Controllers\Admin\DashboardController;
|
||||||
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\SitemapController;
|
use App\Http\Controllers\Web\SitemapController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
// Homepage
|
// Homepage
|
||||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
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
|
// Job routes
|
||||||
Route::prefix('jobs')->group(function () {
|
Route::prefix('jobs')->group(function () {
|
||||||
Route::get('/', [JobController::class, 'index'])->name('jobs.index');
|
Route::get('/', [JobController::class, 'index'])->name('jobs.index');
|
||||||
Route::get('/remote', [JobController::class, 'remote'])->name('jobs.remote');
|
Route::get('/remote', [JobController::class, 'remote'])->name('jobs.remote');
|
||||||
Route::get('/category/{slug}', [JobController::class, 'category'])->name('jobs.category');
|
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
|
// Sitemap routes
|
||||||
|
|||||||
Reference in New Issue
Block a user