diff --git a/app/Filament/Resources/InternshipResource.php b/app/Filament/Resources/InternshipResource.php new file mode 100644 index 0000000..15028b3 --- /dev/null +++ b/app/Filament/Resources/InternshipResource.php @@ -0,0 +1,125 @@ +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'), + ]; + } +} \ No newline at end of file diff --git a/app/Filament/Resources/InternshipResource/Pages/CreateInternship.php b/app/Filament/Resources/InternshipResource/Pages/CreateInternship.php new file mode 100644 index 0000000..e77b067 --- /dev/null +++ b/app/Filament/Resources/InternshipResource/Pages/CreateInternship.php @@ -0,0 +1,12 @@ +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(), + ]), + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/InternshipsPageController.php b/app/Http/Controllers/InternshipsPageController.php index 735a34f..2fb05c6 100644 --- a/app/Http/Controllers/InternshipsPageController.php +++ b/app/Http/Controllers/InternshipsPageController.php @@ -4,17 +4,44 @@ namespace App\Http\Controllers; use App\Models\Internship; use Illuminate\Http\Request; +use App\Models\InternshipApplication; // Changed from App\Models\Application class InternshipsPageController extends Controller { 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) { - 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) diff --git a/app/Models/Internship.php b/app/Models/Internship.php index b1d105a..609055e 100644 --- a/app/Models/Internship.php +++ b/app/Models/Internship.php @@ -3,8 +3,26 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; +use App\Models\InternshipApplication; 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); + } } diff --git a/app/Models/InternshipApplication.php b/app/Models/InternshipApplication.php new file mode 100644 index 0000000..295a6d6 --- /dev/null +++ b/app/Models/InternshipApplication.php @@ -0,0 +1,24 @@ +belongsTo(Internship::class); + } +} \ No newline at end of file diff --git a/config/livewire.php b/config/livewire.php index af15a0a..88e0947 100644 --- a/config/livewire.php +++ b/config/livewire.php @@ -3,33 +3,158 @@ return [ /* - |-------------------------------------------------------------------------- - | Livewire Temporary File Uploads - |-------------------------------------------------------------------------- + |--------------------------------------------------------------------------- + | Class Namespace + |--------------------------------------------------------------------------- | - | By default, Livewire will upload temporary files to the local disk - | and clear them after a maximum of 5 minutes. You may customize - | this process by changing the "disk" and "max_upload_time" properties. + | This value sets the root class namespace for Livewire component classes in + | your application. This value will change where component auto-discovery + | 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' => [ - 'disk' => 'public', // This is the default disk for temporary uploads. - 'rules' => null, // Set this to null to use default upload rules (e.g., file size, mime types). - 'middleware' => null, // Set this to null to use default middleware for temporary uploads. - 'max_upload_time' => 30, // The maximum number of minutes a temporary file can be stored. + 'disk' => null, // Example: 'local', 's3' | Default: 'default' + 'rules' => ['required', 'file', 'max:100000'], // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) + 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' + '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. - | If you're using a custom domain, you may need to change this. + | This value determines if Livewire will run a component's `render()` method + | 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, -]; \ No newline at end of file + /* + |--------------------------------------------------------------------------- + | 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 + |
and 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', +]; diff --git a/database/migrations/2025_07_29_122320_add_internship_id_to_applications_table.php b/database/migrations/2025_07_29_122320_add_internship_id_to_applications_table.php new file mode 100644 index 0000000..56a05e3 --- /dev/null +++ b/database/migrations/2025_07_29_122320_add_internship_id_to_applications_table.php @@ -0,0 +1,28 @@ +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'); + }); + } +}; diff --git a/database/migrations/2025_07_29_122832_add_columns_to_internships_table.php b/database/migrations/2025_07_29_122832_add_columns_to_internships_table.php new file mode 100644 index 0000000..c900ff4 --- /dev/null +++ b/database/migrations/2025_07_29_122832_add_columns_to_internships_table.php @@ -0,0 +1,40 @@ +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', + ]); + }); + } +}; diff --git a/database/migrations/2025_07_29_123157_create_internship_applications_table.php b/database/migrations/2025_07_29_123157_create_internship_applications_table.php new file mode 100644 index 0000000..7e44052 --- /dev/null +++ b/database/migrations/2025_07_29_123157_create_internship_applications_table.php @@ -0,0 +1,34 @@ +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'); + } +}; diff --git a/database/migrations/2025_07_29_123220_drop_internship_id_from_applications_table.php b/database/migrations/2025_07_29_123220_drop_internship_id_from_applications_table.php new file mode 100644 index 0000000..aadba3d --- /dev/null +++ b/database/migrations/2025_07_29_123220_drop_internship_id_from_applications_table.php @@ -0,0 +1,29 @@ +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'); + }); + } +}; diff --git a/resources/views/web/components/application_modal.blade.php b/resources/views/web/components/application_modal.blade.php new file mode 100644 index 0000000..a87f873 --- /dev/null +++ b/resources/views/web/components/application_modal.blade.php @@ -0,0 +1,122 @@ + + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/resources/views/web/pages/careers/index.blade.php b/resources/views/web/pages/careers/index.blade.php index c4d081c..62a5761 100644 --- a/resources/views/web/pages/careers/index.blade.php +++ b/resources/views/web/pages/careers/index.blade.php @@ -47,7 +47,7 @@{{ __('Location:') }} {{ $career->location }}
+{{ __('Salary:') }} {{ $career->salary_per_month }} {{ $career->salary_currency }}
+ +{{ $career->title_description }}
+ + @if ($career->bullets) +{{ __('Location:') }} {{ $internship->location }}
+{{ __('Salary:') }} {{ $internship->salary_per_month }} {{ $internship->salary_currency }}
+ +{{ $internship->title_description }}
+ + @if ($internship->bullets) +