Enhance internship application process: update InternshipsPageController to retrieve internships and validate application submissions, modify Internship model to include relationships, and implement dynamic application modals in views for internships and careers. Update Livewire configuration for improved file upload handling.

This commit is contained in:
2025-07-29 12:37:25 +05:00
parent 9603402fee
commit f6ddf1d9c1
18 changed files with 986 additions and 119 deletions

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\InternshipResource\Pages;
use App\Filament\Resources\InternshipResource\RelationManagers;
use App\Models\Internship;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
class InternshipResource extends Resource
{
protected static ?string $model = Internship::class;
protected static ?string $navigationGroup = 'Internships';
protected static ?string $navigationIcon = 'heroicon-o-academic-cap';
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('title')
->required()
->maxLength(255),
TextInput::make('location')
->required()
->maxLength(255),
Textarea::make('title_description')
->label('Description (show on modal)')
->required()
->maxLength(65535)
->columnSpan('full'),
TextInput::make('salary_per_month')
->required()
->numeric()
->label('Salary per month')
->maxLength(255),
Forms\Components\Select::make('salary_currency')
->options(getCurrencies())
->required()
->label('Salary currency')
->searchable()
->default('USD'),
Repeater::make('bullets')
->schema([
TextInput::make('bullet')
->required()
->maxLength(255),
])
->minItems(1)
->defaultItems(1)
->columnSpan('full'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->searchable(),
Tables\Columns\TextColumn::make('title_description')
->searchable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('salary_per_month')
->searchable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('salary_currency')
->searchable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('location')
->searchable(),
Tables\Columns\TextColumn::make('salary')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
RelationManagers\ApplicationsRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListInternships::route('/'),
'create' => Pages\CreateInternship::route('/create'),
'edit' => Pages\EditInternship::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Filament\Resources\InternshipResource\Pages;
use App\Filament\Resources\InternshipResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateInternship extends CreateRecord
{
protected static string $resource = InternshipResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\InternshipResource\Pages;
use App\Filament\Resources\InternshipResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditInternship extends EditRecord
{
protected static string $resource = InternshipResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\InternshipResource\Pages;
use App\Filament\Resources\InternshipResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListInternships extends ListRecords
{
protected static string $resource = InternshipResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Filament\Resources\InternshipResource\RelationManagers;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use App\Models\InternshipApplication; // Added this use statement
class ApplicationsRelationManager extends RelationManager
{
protected static string $relationship = 'applications';
// Add this line to define the inverse relationship
protected static ?string $model = InternshipApplication::class;
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\DatePicker::make('birthdate')
->required(),
Forms\Components\FileUpload::make('resume_file')
->required()
->acceptedFileTypes(['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'])
->disk('public') // or your preferred disk
->directory('resumes'),
Forms\Components\TextInput::make('email')
->email()
->required()
->maxLength(255),
Forms\Components\TextInput::make('phone_number')
->required()
->maxLength(20),
Forms\Components\Textarea::make('cover_letter')
->maxLength(65535)
->nullable()
->columnSpan('full'),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('name')
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('email')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('phone_number')
->searchable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -4,17 +4,44 @@ namespace App\Http\Controllers;
use App\Models\Internship; use App\Models\Internship;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\InternshipApplication; // Changed from App\Models\Application
class InternshipsPageController extends Controller class InternshipsPageController extends Controller
{ {
public function index() public function index()
{ {
return view('web.pages.internships.index'); $internships = Internship::query()->get();
return view('web.pages.internships.index', compact('internships'));
} }
public function store(Request $request) public function store(Request $request)
{ {
dd($request->all()); $validatedData = $request->validate([
'internship_id' => 'required|exists:internships,id',
'name' => 'required|string|max:255',
'birthdate' => 'required|date',
'resume_file' => 'required|file|mimes:pdf,doc,docx|max:2048',
'email' => 'required|email|max:255',
'phone_number' => 'required|string',
'cover_letter' => 'nullable|string',
]);
$resumePath = $request->file('resume_file')->store('resumes');
InternshipApplication::create([ // Changed to InternshipApplication::create
'internship_id' => $validatedData['internship_id'],
'name' => $validatedData['name'],
'birthdate' => $validatedData['birthdate'],
'resume_file' => $resumePath,
'email' => $validatedData['email'],
'phone_number' => $validatedData['phone_number'],
'cover_letter' => $validatedData['cover_letter'] ?? null,
]);
return response()->json([
'message' => 'Your application has been submitted successfully!',
]);
} }
public function show(Internship $internship) public function show(Internship $internship)

View File

@@ -3,8 +3,26 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Models\InternshipApplication;
class Internship extends Model class Internship extends Model
{ {
// protected $fillable = [
'title',
'title_description',
'salary_per_month',
'bullets',
'location',
'salary_currency',
];
protected $casts = [
'bullets' => 'array',
];
public function applications(): HasMany
{
return $this->hasMany(InternshipApplication::class);
}
} }

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class InternshipApplication extends Model
{
protected $fillable = [
'internship_id',
'name',
'birthdate',
'resume_file',
'email',
'phone_number',
'cover_letter',
];
public function internship(): BelongsTo
{
return $this->belongsTo(Internship::class);
}
}

View File

@@ -3,33 +3,158 @@
return [ return [
/* /*
|-------------------------------------------------------------------------- |---------------------------------------------------------------------------
| Livewire Temporary File Uploads | Class Namespace
|-------------------------------------------------------------------------- |---------------------------------------------------------------------------
| |
| By default, Livewire will upload temporary files to the local disk | This value sets the root class namespace for Livewire component classes in
| and clear them after a maximum of 5 minutes. You may customize | your application. This value will change where component auto-discovery
| this process by changing the "disk" and "max_upload_time" properties. | finds components. It's also referenced by the file creation commands.
|
*/
'class_namespace' => 'App\\Livewire',
/*
|---------------------------------------------------------------------------
| View Path
|---------------------------------------------------------------------------
|
| This value is used to specify where Livewire component Blade templates are
| stored when running file creation commands like `artisan make:livewire`.
| It is also used if you choose to omit a component's render() method.
|
*/
'view_path' => resource_path('views/livewire'),
/*
|---------------------------------------------------------------------------
| Layout
|---------------------------------------------------------------------------
| The view that will be used as the layout when rendering a single component
| as an entire page via `Route::get('/post/create', CreatePost::class);`.
| In this case, the view returned by CreatePost will render into $slot.
|
*/
'layout' => 'components.layouts.app',
/*
|---------------------------------------------------------------------------
| Lazy Loading Placeholder
|---------------------------------------------------------------------------
| Livewire allows you to lazy load components that would otherwise slow down
| the initial page load. Every component can have a custom placeholder or
| you can define the default placeholder view for all components below.
|
*/
'lazy_placeholder' => null,
/*
|---------------------------------------------------------------------------
| Temporary File Uploads
|---------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is stored permanently. All file uploads are directed to
| a global endpoint for temporary storage. You may configure this below:
| |
*/ */
'temporary_file_upload' => [ 'temporary_file_upload' => [
'disk' => 'public', // This is the default disk for temporary uploads. 'disk' => null, // Example: 'local', 's3' | Default: 'default'
'rules' => null, // Set this to null to use default upload rules (e.g., file size, mime types). 'rules' => ['required', 'file', 'max:100000'], // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'middleware' => null, // Set this to null to use default middleware for temporary uploads. 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
'max_upload_time' => 30, // The maximum number of minutes a temporary file can be stored. 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 20, // Max duration (in minutes) before an upload is invalidated...
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
], ],
/* /*
|-------------------------------------------------------------------------- |---------------------------------------------------------------------------
| Livewire App URL | Render On Redirect
|-------------------------------------------------------------------------- |---------------------------------------------------------------------------
| |
| This is the app URL that Livewire will use to make requests to. | This value determines if Livewire will run a component's `render()` method
| If you're using a custom domain, you may need to change this. | after a redirect has been triggered using something like `redirect(...)`
| Setting this to true will render the view once more before redirecting
| |
*/ */
'app_url' => env('APP_URL'), 'render_on_redirect' => false,
]; /*
|---------------------------------------------------------------------------
| Eloquent Model Binding
|---------------------------------------------------------------------------
|
| Previous versions of Livewire supported binding directly to eloquent model
| properties using wire:model by default. However, this behavior has been
| deemed too "magical" and has therefore been put under a feature flag.
|
*/
'legacy_model_binding' => false,
/*
|---------------------------------------------------------------------------
| Auto-inject Frontend Assets
|---------------------------------------------------------------------------
|
| By default, Livewire automatically injects its JavaScript and CSS into the
| <head> and <body> of pages containing Livewire components. By disabling
| this behavior, you need to use @livewireStyles and @livewireScripts.
|
*/
'inject_assets' => true,
/*
|---------------------------------------------------------------------------
| Navigate (SPA mode)
|---------------------------------------------------------------------------
|
| By adding `wire:navigate` to links in your Livewire application, Livewire
| will prevent the default link handling and instead request those pages
| via AJAX, creating an SPA-like effect. Configure this behavior here.
|
*/
'navigate' => [
'show_progress_bar' => true,
'progress_bar_color' => '#2299dd',
],
/*
|---------------------------------------------------------------------------
| HTML Morph Markers
|---------------------------------------------------------------------------
|
| Livewire intelligently "morphs" existing HTML into the newly rendered HTML
| after each update. To make this process more reliable, Livewire injects
| "markers" into the rendered Blade surrounding @if, @class & @foreach.
|
*/
'inject_morph_markers' => true,
/*
|---------------------------------------------------------------------------
| Pagination Theme
|---------------------------------------------------------------------------
|
| When enabling Livewire's pagination feature by using the `WithPagination`
| trait, Livewire will use Tailwind templates to render pagination views
| on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
|
*/
'pagination_theme' => 'tailwind',
];

View File

@@ -0,0 +1,28 @@
<?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('applications', function (Blueprint $table) {
$table->foreignId('internship_id')->nullable()->constrained()->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropConstrainedForeignId('internship_id');
});
}
};

View File

@@ -0,0 +1,40 @@
<?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('internships', function (Blueprint $table) {
$table->string('title');
$table->text('title_description');
$table->string('salary_per_month');
$table->json('bullets');
$table->string('location');
$table->string('salary_currency');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('internships', function (Blueprint $table) {
$table->dropColumn([
'title',
'title_description',
'salary_per_month',
'bullets',
'location',
'salary_currency',
]);
});
}
};

View File

@@ -0,0 +1,34 @@
<?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::create('internship_applications', function (Blueprint $table) {
$table->id();
$table->foreignId('internship_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->date('birthdate');
$table->string('resume_file');
$table->string('email');
$table->string('phone_number');
$table->text('cover_letter')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('internship_applications');
}
};

View File

@@ -0,0 +1,29 @@
<?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('applications', function (Blueprint $table) {
$table->dropForeign(['internship_id']);
$table->dropColumn('internship_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->foreignId('internship_id')->nullable()->constrained()->onDelete('set null');
});
}
};

View File

@@ -0,0 +1,122 @@
<div class="modal fade" id="applyInternshipModal" tabindex="-1" aria-labelledby="applyInternshipModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content rounded-3">
<div class="modal-header">
<h5 class="modal-title" id="applicationModalLabel">Apply for <span id="jobTitle"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h6 id="jobLocation"></h6>
<p id="jobSalary"></p>
<p id="jobDescription"></p>
<form id="applicationForm" enctype="multipart/form-data" action="{{ $jobType === 'internship' ? route('internship.store') : route('career.store') }}" method="POST">
@csrf
@if($jobType === 'internship')
<input type="hidden" name="internship_id" id="jobIdInput" value="{{ $jobId }}">
@else
<input type="hidden" name="career_id" id="jobIdInput" value="{{ $jobId }}">
@endif
<div class="row">
<div class="col-md-6 mb-3">
<label for="name" class="form-label">Name <span> *</span></label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="col-md-6 mb-3">
<label for="birthdate" class="form-label">Birthdate <span> *</span></label>
<input type="date" class="form-control" id="birthdate" name="birthdate" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email <span> *</span></label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="col-md-6 mb-3">
<label for="phone_number" class="form-label">Phone Number <span> *</span></label>
<input type="text" class="form-control" id="phone_number" name="phone_number" required>
</div>
</div>
<div class="mb-3">
<label for="resume_file" class="form-label">Resume (PDF, DOC, DOCX) <span> *</span></label>
<input type="file" class="form-control" id="resume_file" name="resume_file" accept=".pdf,.doc,.docx" required>
</div>
<div class="mb-3">
<label for="cover_letter" class="form-label">Cover Letter (Optional)</label>
<textarea class="form-control" id="cover_letter" name="cover_letter" rows="5"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit Application</button>
</form>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
var applicationModal = document.getElementById('applyInternshipModal'); // Changed ID here
if (applicationModal) { // Add a check for modal existence
applicationModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget; // Button that triggered the modal
var jobId = button.getAttribute('data-job-id'); // Generic attribute for job ID
var jobType = button.getAttribute('data-job-type'); // Generic attribute for job type
var modalTitle = applicationModal.querySelector('#jobTitle');
var modalLocation = applicationModal.querySelector('#jobLocation');
var modalSalary = applicationModal.querySelector('#jobSalary');
var modalDescription = applicationModal.querySelector('#jobDescription');
var jobIdInput = applicationModal.querySelector('#jobIdInput');
var applicationForm = applicationModal.querySelector('#applicationForm');
// Set dynamic values
modalTitle.textContent = button.getAttribute('data-' + jobType + '-title');
modalLocation.textContent = 'Location: ' + button.getAttribute('data-' + jobType + '-location');
modalSalary.textContent = 'Salary: ' + button.getAttribute('data-' + jobType + '-salary') + ' / Per monthly';
modalDescription.textContent = button.getAttribute('data-' + jobType + '-description');
jobIdInput.value = jobId;
// Set form action dynamically
if (jobType === 'internship') {
applicationForm.action = '{{ route('internship.store') }}';
jobIdInput.name = 'internship_id';
} else if (jobType === 'career') {
applicationForm.action = '{{ route('applications.store') }}'; // Assuming careers still use applications.store
jobIdInput.name = 'career_id';
}
});
// Handle form submission with AJAX
document.getElementById('applicationForm').addEventListener('submit', function (e) {
e.preventDefault();
let form = e.target;
let formData = new FormData(form);
fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => response.json())
.then(data => {
if (data.message) {
alert(data.message);
var modal = bootstrap.Modal.getInstance(applicationModal);
modal.hide();
form.reset();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred. Please try again.');
});
});
}
});
</script>
@endpush

View File

@@ -47,7 +47,7 @@
</ul> </ul>
</div> </div>
<div class="price__area-item-btn"> <div class="price__area-item-btn">
<button type="button" class="build_button apply-now-button" data-bs-toggle="modal" data-bs-target="#applicationModal" data-career-title="{{ $career->title }}" data-career-location="{{ $career->location }}" data-career-salary="{{ $career->salary_per_month }}" data-career-description="{{ $career->title_description }}" data-career-id="{{ $career->id }}">Apply Now<i class="flaticon-right-up"></i></button> <button type="button" class="build_button apply-now-button" data-bs-toggle="modal" data-bs-target="#applyInternshipModal" data-job-id="{{ $career->id }}" data-job-type="career" data-career-title="{{ $career->title }}" data-career-location="{{ $career->location }}" data-career-salary="{{ $career->salary_per_month }}" data-career-description="{{ $career->title_description }}">Apply Now<i class="flaticon-right-up"></i></button>
</div> </div>
</div> </div>
@empty @empty
@@ -59,114 +59,77 @@
</div> </div>
<!-- Pricing Plan Area End --> <!-- Pricing Plan Area End -->
<!-- Application Modal -->
<div class="modal fade" id="applicationModal" tabindex="-1" aria-labelledby="applicationModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content rounded-3">
<div class="modal-header">
<h5 class="modal-title" id="applicationModalLabel">Apply for <span id="jobTitle"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h6 id="jobLocation"></h6>
<p id="jobSalary"></p>
<p id="jobDescription"></p>
<form id="applicationForm" enctype="multipart/form-data" action="{{ route('applications.store') }}" method="POST">
@csrf
<input type="hidden" name="career_id" id="careerId">
<div class="row">
<div class="col-md-6 mb-3">
<label for="name" class="form-label">Name <span> *</span></label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="col-md-6 mb-3">
<label for="birthdate" class="form-label">Birthdate <span> *</span></label>
<input type="date" class="form-control" id="birthdate" name="birthdate" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email <span> *</span></label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="col-md-6 mb-3">
<label for="phone_number" class="form-label">Phone Number <span> *</span></label>
<input type="text" class="form-control" id="phone_number" name="phone_number" required>
</div>
</div>
<div class="mb-3">
<label for="resume_file" class="form-label">Resume (PDF, DOC, DOCX) <span> *</span></label>
<input type="file" class="form-control" id="resume_file" name="resume_file" accept=".pdf,.doc,.docx" required>
</div>
<div class="mb-3">
<label for="cover_letter" class="form-label">Cover Letter (Optional)</label>
<textarea class="form-control" id="cover_letter" name="cover_letter" rows="5"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit Application</button>
</form>
</div>
</div>
</div>
</div>
@endsection @endsection
{{-- Application Modal --}}
@include('web.components.application_modal', ['jobId' => null, 'jobType' => 'career']) {{-- jobId will be set by JS from button data attributes --}}
@push('footer-js') @push('footer-js')
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
var applicationModal = document.getElementById('applicationModal'); var applicationModal = document.getElementById('applyInternshipModal');
applicationModal.addEventListener('show.bs.modal', function (event) { if (applicationModal) {
var button = event.relatedTarget; applicationModal.addEventListener('show.bs.modal', function (event) {
var careerTitle = button.getAttribute('data-career-title'); var button = event.relatedTarget; // Button that triggered the modal
var careerLocation = button.getAttribute('data-career-location'); var jobId = button.getAttribute('data-job-id'); // Generic attribute for job ID
var careerSalary = button.getAttribute('data-career-salary'); var jobType = button.getAttribute('data-job-type'); // Generic attribute for job type
var careerDescription = button.getAttribute('data-career-description');
var careerId = button.getAttribute('data-career-id');
var modalTitle = applicationModal.querySelector('#jobTitle'); var modalTitle = applicationModal.querySelector('#jobTitle');
var modalLocation = applicationModal.querySelector('#jobLocation'); var modalLocation = applicationModal.querySelector('#jobLocation');
var modalSalary = applicationModal.querySelector('#jobSalary'); var modalSalary = applicationModal.querySelector('#jobSalary');
var modalDescription = applicationModal.querySelector('#jobDescription'); var modalDescription = applicationModal.querySelector('#jobDescription');
var modalCareerId = applicationModal.querySelector('#careerId'); var jobIdInput = applicationModal.querySelector('#jobIdInput');
var applicationForm = applicationModal.querySelector('#applicationForm');
modalTitle.textContent = careerTitle; // Set dynamic values
modalLocation.textContent = 'Location: ' + careerLocation; modalTitle.textContent = button.getAttribute('data-' + jobType + '-title');
modalSalary.textContent = 'Salary: ' + careerSalary + ' / Per monthly'; modalLocation.textContent = 'Location: ' + button.getAttribute('data-' + jobType + '-location');
modalDescription.textContent = careerDescription; modalSalary.textContent = 'Salary: ' + button.getAttribute('data-' + jobType + '-salary') + ' / Per monthly';
modalCareerId.value = careerId; modalDescription.textContent = button.getAttribute('data-' + jobType + '-description');
}); jobIdInput.value = jobId;
// Handle form submission with AJAX // Set form action dynamically
document.getElementById('applicationForm').addEventListener('submit', function (e) { if (jobType === 'internship') {
e.preventDefault(); applicationForm.action = '{{ route('internship.store') }}';
jobIdInput.name = 'internship_id';
let form = e.target; } else if (jobType === 'career') {
let formData = new FormData(form); applicationForm.action = '{{ route('applications.store') }}'; // Assuming careers still use applications.store
jobIdInput.name = 'career_id';
fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
} }
})
.then(response => response.json())
.then(data => {
if (data.message) {
alert(data.message);
var modal = bootstrap.Modal.getInstance(applicationModal);
modal.hide();
form.reset();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred. Please try again.');
}); });
});
// Handle form submission with AJAX
document.getElementById('applicationForm').addEventListener('submit', function (e) {
e.preventDefault();
let form = e.target;
let formData = new FormData(form);
fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => response.json())
.then(data => {
if (data.message) {
alert(data.message);
var modal = bootstrap.Modal.getInstance(applicationModal);
modal.hide();
form.reset();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred. Please try again.');
});
});
}
}); });
</script> </script>
@endpush @endpush

View File

@@ -0,0 +1,32 @@
@extends('web.layouts.app')
@section('title', $career->title)
@section('content')
<!-- Main Content for Single Career Page -->
<div class="container">
<h1>{{ $career->title }}</h1>
<p><strong>{{ __('Location:') }}</strong> {{ $career->location }}</p>
<p><strong>{{ __('Salary:') }}</strong> {{ $career->salary_per_month }} {{ $career->salary_currency }}</p>
<h2>{{ __('Description') }}</h2>
<p>{{ $career->title_description }}</p>
@if ($career->bullets)
<h3>{{ __('Responsibilities and Qualifications') }}</h3>
<ul>
@foreach ($career->bullets as $bullet)
<li>{{ $bullet['bullet'] }}</li>
@endforeach
</ul>
@endif
<!-- Apply Button - Triggers Modal -->
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#applyInternshipModal">
{{ __('Apply for this Career') }}
</button>
</div>
<!-- Application Modal -->
@include('web.components.application_modal', ['jobId' => $career->id, 'jobType' => 'career'])
@endsection

View File

@@ -0,0 +1,135 @@
@extends('web.layouts.app')
@push('header-css')
<style>
#applicationForm > label > span {
color: var(--gujurly-primary);
}
</style>
@endpush
@section('content')
<!-- Breadcrumb Area Start -->
<div class="breadcrumb__area" style="background-image: url('/web/assets/img/page/breadcrumb.jpg');">
<div class="container">
<div class="row">
<div class="col-xl-12">
<div class="breadcrumb__area-content">
<h2>Internships</h2>
<ul>
<li><a href="/">Home</a><i class="fa-regular fa-angle-right"></i></li>
<li>Internships</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Breadcrumb Area End -->
<!-- Pricing Plan Area Start -->
<div class="price__area section-padding">
<div class="container">
<div class="row">
<div class="col-xl-4 col-md-6 xl-mb-25 wow fadeInUp" data-wow-delay=".4s">
@forelse ($internships as $internship)
<div class="price__area-item">
<div class="price__area-item-price">
<span>{{ $internship->title }}</span>
<h3>{{ $internship->location }}</h3>
<h2>{{ $internship->salary_per_month }} {{ $internship->salary_currency }}</h2>
<span>Per monthly</span>
</div>
<div class="price__area-item-list">
<ul>
@foreach($internship->bullets as $bullet)
<li><i class="flaticon-checked"></i>{{ $bullet['bullet'] ?? '' }}</li>
@endforeach
</ul>
</div>
<div class="price__area-item-btn">
<button type="button" class="build_button apply-now-button" data-bs-toggle="modal" data-bs-target="#applyInternshipModal" data-job-id="{{ $internship->id }}" data-job-type="internship" data-internship-title="{{ $internship->title }}" data-internship-location="{{ $internship->location }}" data-internship-salary="{{ $internship->salary_per_month }}" data-internship-description="{{ $internship->title_description }}">Apply Now<i class="flaticon-right-up"></i></button>
</div>
</div>
@empty
<span>No internships found...</span>
@endforelse
</div>
</div>
</div>
</div>
<!-- Pricing Plan Area End -->
{{-- Application Modal --}}
@include('web.components.application_modal', ['jobId' => null, 'jobType' => 'internship']) {{-- jobId will be set by JS from button data attributes --}}
@endsection
@push('footer-js')
<script>
document.addEventListener('DOMContentLoaded', function () {
var applicationModal = document.getElementById('applyInternshipModal');
if (applicationModal) {
applicationModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget; // Button that triggered the modal
var jobId = button.getAttribute('data-job-id'); // Generic attribute for job ID
var jobType = button.getAttribute('data-job-type'); // Generic attribute for job type
var modalTitle = applicationModal.querySelector('#jobTitle');
var modalLocation = applicationModal.querySelector('#jobLocation');
var modalSalary = applicationModal.querySelector('#jobSalary');
var modalDescription = applicationModal.querySelector('#jobDescription');
var jobIdInput = applicationModal.querySelector('#jobIdInput');
var applicationForm = applicationModal.querySelector('#applicationForm');
// Set dynamic values
modalTitle.textContent = button.getAttribute('data-' + jobType + '-title');
modalLocation.textContent = 'Location: ' + button.getAttribute('data-' + jobType + '-location');
modalSalary.textContent = 'Salary: ' + button.getAttribute('data-' + jobType + '-salary') + ' / Per monthly';
modalDescription.textContent = button.getAttribute('data-' + jobType + '-description');
jobIdInput.value = jobId;
// Set form action dynamically
if (jobType === 'internship') {
applicationForm.action = '{{ route('internship.store') }}';
jobIdInput.name = 'internship_id';
} else if (jobType === 'career') {
applicationForm.action = '{{ route('applications.store') }}'; // Assuming careers still use applications.store
jobIdInput.name = 'career_id';
}
});
// Handle form submission with AJAX
document.getElementById('applicationForm').addEventListener('submit', function (e) {
e.preventDefault();
let form = e.target;
let formData = new FormData(form);
fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => response.json())
.then(data => {
if (data.message) {
alert(data.message);
var modal = bootstrap.Modal.getInstance(applicationModal);
modal.hide();
form.reset();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred. Please try again.');
});
});
}
});
</script>
@endpush

View File

@@ -0,0 +1,32 @@
@extends('web.layouts.app')
@section('title', $internship->title)
@section('content')
<!-- Main Content for Single Internship Page -->
<div class="container">
<h1>{{ $internship->title }}</h1>
<p><strong>{{ __('Location:') }}</strong> {{ $internship->location }}</p>
<p><strong>{{ __('Salary:') }}</strong> {{ $internship->salary_per_month }} {{ $internship->salary_currency }}</p>
<h2>{{ __('Description') }}</h2>
<p>{{ $internship->title_description }}</p>
@if ($internship->bullets)
<h3>{{ __('Responsibilities and Qualifications') }}</h3>
<ul>
@foreach ($internship->bullets as $bullet)
<li>{{ $bullet['bullet'] }}</li>
@endforeach
</ul>
@endif
<!-- Apply Button - Triggers Modal -->
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#applyInternshipModal">
{{ __('Apply for this Internship') }}
</button>
</div>
<!-- Application Modal -->
@include('web.components.application_modal', ['jobId' => $internship->id, 'jobType' => 'internship'])
@endsection