added the cropping library
This commit is contained in:
parent
c1fc28087e
commit
7a18eb6588
120
INSTALLATION_SUMMARY.md
Normal file
120
INSTALLATION_SUMMARY.md
Normal file
@ -0,0 +1,120 @@
|
||||
# Laravel Image Cropper Installation Summary
|
||||
|
||||
## Package Installed
|
||||
- **Package**: `takeone/cropper` from https://git.innovator.bh/ghassan/laravel-image-cropper
|
||||
- **Version**: dev-main
|
||||
- **Installation Date**: January 26, 2026
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. composer.json
|
||||
- Added VCS repository for the package
|
||||
- Installed `takeone/cropper:@dev`
|
||||
|
||||
### 2. resources/views/layouts/app.blade.php
|
||||
- Added `@stack('modals')` before closing `</body>` tag
|
||||
- This allows the cropper modal to be injected into the page
|
||||
|
||||
### 3. resources/views/family/profile-edit.blade.php
|
||||
- Replaced the old `<x-image-upload-modal>` component with `<x-takeone-cropper>`
|
||||
- Configured cropper with:
|
||||
- **ID**: `profile_picture`
|
||||
- **Width**: 300px
|
||||
- **Height**: 400px (portrait rectangle - 3:4 ratio)
|
||||
- **Shape**: `square` (rectangle viewport)
|
||||
- **Folder**: `images/profiles`
|
||||
- **Filename**: `profile_{user_id}`
|
||||
- **Upload URL**: Custom route `profile.upload-picture`
|
||||
- Added image display logic to show current profile picture or placeholder
|
||||
|
||||
### 4. app/Http/Controllers/FamilyController.php
|
||||
- Updated `uploadProfilePicture()` method to handle base64 image data from cropper
|
||||
- Method now:
|
||||
- Accepts base64 image data instead of file upload
|
||||
- Decodes and saves the cropped image
|
||||
- Updates user's `profile_picture` field in database
|
||||
- Returns JSON response with success status and image path
|
||||
|
||||
### 5. resources/views/vendor/takeone/components/widget.blade.php
|
||||
- Published and customized the package's widget component
|
||||
- Added support for custom `uploadUrl` parameter
|
||||
- Added page reload after successful upload to display new image
|
||||
- Improved error handling with detailed error messages
|
||||
|
||||
### 6. Storage
|
||||
- Ran `php artisan storage:link` to link public storage
|
||||
|
||||
## How It Works
|
||||
|
||||
1. User clicks "Change Photo" button on profile edit page
|
||||
2. Modal popup appears with file selector
|
||||
3. User selects an image file
|
||||
4. Cropme.js library loads the image in a cropping interface
|
||||
5. User can:
|
||||
- Zoom in/out using slider
|
||||
- Rotate image using slider
|
||||
- Pan/move image within the viewport
|
||||
6. Cropping viewport is set to 300x400px (portrait rectangle)
|
||||
7. User clicks "Crop & Save Image"
|
||||
8. Image is cropped to base64 format
|
||||
9. AJAX POST request sent to `/profile/upload-picture`
|
||||
10. FamilyController processes the base64 image:
|
||||
- Decodes base64 data
|
||||
- Saves to `storage/app/public/images/profiles/profile_{user_id}.{ext}`
|
||||
- Deletes old profile picture if exists
|
||||
- Updates user's `profile_picture` field
|
||||
11. Page reloads to display the new profile picture
|
||||
|
||||
## File Locations
|
||||
|
||||
- **Uploaded Images**: `storage/app/public/images/profiles/`
|
||||
- **Public Access**: `public/storage/images/profiles/` (via symlink)
|
||||
- **Database Field**: `users.profile_picture`
|
||||
|
||||
## Portrait Rectangle Configuration
|
||||
|
||||
The cropper is configured for portrait orientation:
|
||||
- **Aspect Ratio**: 3:4 (300px width × 400px height)
|
||||
- **Shape**: `square` (rectangle viewport, not circular)
|
||||
- **Viewport**: Displays as a rectangle overlay on the image
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [x] Package installed successfully
|
||||
- [x] Storage linked
|
||||
- [x] Modal stack added to layout
|
||||
- [x] Cropper component integrated
|
||||
- [x] Custom upload route configured
|
||||
- [x] Controller method updated
|
||||
- [ ] Test image upload
|
||||
- [ ] Verify cropped image saves correctly
|
||||
- [ ] Confirm image displays after upload
|
||||
- [ ] Test portrait rectangle cropping
|
||||
- [ ] Verify old images are deleted
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Navigate to http://localhost:8000/profile/edit
|
||||
2. Click "Change Photo" button
|
||||
3. Select an image
|
||||
4. Crop in portrait rectangle shape
|
||||
5. Save and verify the image appears correctly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the cropper doesn't appear:
|
||||
- Check browser console for JavaScript errors
|
||||
- Verify `@stack('modals')` is in layout
|
||||
- Ensure jQuery is loaded before the cropper script
|
||||
|
||||
If upload fails:
|
||||
- Check `storage/app/public/images/profiles/` directory exists
|
||||
- Verify storage is linked: `php artisan storage:link`
|
||||
- Check file permissions on storage directory
|
||||
- Review Laravel logs in `storage/logs/`
|
||||
|
||||
## Package Documentation
|
||||
|
||||
For more details, see:
|
||||
- Package README: `vendor/takeone/cropper/README.md`
|
||||
- Package Example: `vendor/takeone/cropper/EXAMPLE.md`
|
||||
251
TESTING_GUIDE.md
Normal file
251
TESTING_GUIDE.md
Normal file
@ -0,0 +1,251 @@
|
||||
# Laravel Image Cropper - Testing Guide
|
||||
|
||||
## Installation Complete ✅
|
||||
|
||||
The Laravel Image Cropper package has been successfully installed and integrated into your project.
|
||||
|
||||
## What Was Installed
|
||||
|
||||
1. **Package**: `takeone/cropper:@dev`
|
||||
2. **Cropper Library**: Cropme.js (lightweight image cropping)
|
||||
3. **Portrait Rectangle Configuration**: 300px × 400px (3:4 ratio)
|
||||
4. **Storage Directory**: `storage/app/public/images/profiles/`
|
||||
|
||||
## Files Modified
|
||||
|
||||
### 1. composer.json
|
||||
- Added VCS repository
|
||||
- Added package dependency
|
||||
|
||||
### 2. resources/views/layouts/app.blade.php
|
||||
- Added `@stack('modals')` for modal injection
|
||||
|
||||
### 3. resources/views/family/profile-edit.blade.php
|
||||
- Replaced old image upload modal with `<x-takeone-cropper>` component
|
||||
- Configured for portrait rectangle cropping
|
||||
- Added image display with fallback to default avatar
|
||||
|
||||
### 4. app/Http/Controllers/FamilyController.php
|
||||
- Updated `uploadProfilePicture()` method to handle base64 images
|
||||
- Saves cropped images to `storage/app/public/images/profiles/`
|
||||
- Updates user's `profile_picture` field in database
|
||||
|
||||
### 5. resources/views/vendor/takeone/components/widget.blade.php
|
||||
- Published and customized widget component
|
||||
- Added custom upload URL support
|
||||
- Added page reload after successful upload
|
||||
- Improved error handling
|
||||
|
||||
## How to Test
|
||||
|
||||
### Step 1: Start the Development Server
|
||||
```bash
|
||||
php artisan serve
|
||||
```
|
||||
|
||||
### Step 2: Navigate to Profile Edit Page
|
||||
Open your browser and go to:
|
||||
```
|
||||
http://localhost:8000/profile/edit
|
||||
```
|
||||
|
||||
### Step 3: Test the Cropper
|
||||
|
||||
1. **Click "Change Photo" button**
|
||||
- A modal should popup with a file selector
|
||||
|
||||
2. **Select an Image**
|
||||
- Click "Choose File" and select any image from your computer
|
||||
- The image should load in the cropping interface
|
||||
|
||||
3. **Crop the Image**
|
||||
- You'll see a **portrait rectangle** overlay (300×400px)
|
||||
- Use the **Zoom Level** slider to zoom in/out
|
||||
- Use the **Rotation** slider to rotate the image
|
||||
- Drag the image to position it within the rectangle
|
||||
|
||||
4. **Save the Image**
|
||||
- Click "Crop & Save Image" button
|
||||
- Button should show "Uploading..." while processing
|
||||
- You should see "Saved successfully!" alert
|
||||
- Modal should close automatically
|
||||
- Page should reload
|
||||
- Your new profile picture should appear in the profile picture box
|
||||
|
||||
### Step 4: Verify the Upload
|
||||
|
||||
Check that the image was saved:
|
||||
```bash
|
||||
dir storage\app\public\images\profiles\
|
||||
```
|
||||
|
||||
You should see a file named `profile_{user_id}.png`
|
||||
|
||||
### Step 5: Verify Database Update
|
||||
|
||||
The `users` table should have the `profile_picture` field updated with:
|
||||
```
|
||||
images/profiles/profile_{user_id}.png
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
### ✅ Success Indicators
|
||||
- Modal opens when clicking "Change Photo"
|
||||
- Image loads in cropper after selection
|
||||
- Portrait rectangle viewport is visible (taller than wide)
|
||||
- Zoom and rotation sliders work smoothly
|
||||
- Image can be dragged/positioned
|
||||
- "Crop & Save Image" button uploads successfully
|
||||
- Success alert appears
|
||||
- Page reloads automatically
|
||||
- New profile picture displays in the profile box
|
||||
|
||||
### ❌ Potential Issues
|
||||
|
||||
**Modal doesn't appear:**
|
||||
- Check browser console (F12) for JavaScript errors
|
||||
- Verify `@stack('modals')` is in `resources/views/layouts/app.blade.php`
|
||||
- Ensure jQuery and Bootstrap are loaded
|
||||
|
||||
**Upload fails:**
|
||||
- Check `storage/app/public/images/profiles/` directory exists
|
||||
- Verify storage link: `php artisan storage:link`
|
||||
- Check file permissions on storage directory
|
||||
- Review Laravel logs: `storage/logs/laravel.log`
|
||||
|
||||
**Image doesn't display after upload:**
|
||||
- Verify storage is linked
|
||||
- Check the `profile_picture` field in database
|
||||
- Ensure the file exists in `public/storage/images/profiles/`
|
||||
- Clear browser cache
|
||||
|
||||
**Cropper shows square instead of rectangle:**
|
||||
- Verify the component has `width="300"` and `height="400"`
|
||||
- Check that `shape="square"` (not "circle")
|
||||
|
||||
## Package Bug Note
|
||||
|
||||
⚠️ **Known Issue**: The package's service provider has a namespace bug in the route registration. This doesn't affect functionality because we're using our own custom route (`profile.upload-picture`) instead of the package's default route.
|
||||
|
||||
The error you might see when running `php artisan route:list`:
|
||||
```
|
||||
Class "takeone\cropper\Http\Controllers\ImageController" does not exist
|
||||
```
|
||||
|
||||
This can be safely ignored as we're not using that route.
|
||||
|
||||
## Customization Options
|
||||
|
||||
### Change Aspect Ratio
|
||||
|
||||
Edit `resources/views/family/profile-edit.blade.php`:
|
||||
|
||||
```html
|
||||
<!-- Current: 3:4 portrait -->
|
||||
<x-takeone-cropper
|
||||
width="300"
|
||||
height="400"
|
||||
/>
|
||||
|
||||
<!-- Square: 1:1 -->
|
||||
<x-takeone-cropper
|
||||
width="300"
|
||||
height="300"
|
||||
/>
|
||||
|
||||
<!-- Wide rectangle: 16:9 -->
|
||||
<x-takeone-cropper
|
||||
width="400"
|
||||
height="225"
|
||||
/>
|
||||
|
||||
<!-- Tall portrait: 2:3 -->
|
||||
<x-takeone-cropper
|
||||
width="300"
|
||||
height="450"
|
||||
/>
|
||||
```
|
||||
|
||||
### Change to Circular Crop
|
||||
|
||||
```html
|
||||
<x-takeone-cropper
|
||||
width="300"
|
||||
height="300"
|
||||
shape="circle"
|
||||
/>
|
||||
```
|
||||
|
||||
### Change Storage Folder
|
||||
|
||||
```html
|
||||
<x-takeone-cropper
|
||||
folder="avatars"
|
||||
/>
|
||||
```
|
||||
|
||||
### Custom Filename Pattern
|
||||
|
||||
```html
|
||||
<x-takeone-cropper
|
||||
filename="user_{{ auth()->id() }}_{{ time() }}"
|
||||
/>
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
takeone/
|
||||
├── storage/
|
||||
│ └── app/
|
||||
│ └── public/
|
||||
│ └── images/
|
||||
│ └── profiles/ ← Uploaded images here
|
||||
│ └── profile_1.png
|
||||
├── public/
|
||||
│ └── storage/ ← Symlink to storage/app/public
|
||||
│ └── images/
|
||||
│ └── profiles/
|
||||
│ └── profile_1.png ← Publicly accessible
|
||||
├── resources/
|
||||
│ └── views/
|
||||
│ ├── family/
|
||||
│ │ └── profile-edit.blade.php ← Profile edit page
|
||||
│ ├── layouts/
|
||||
│ │ └── app.blade.php ← Main layout with @stack('modals')
|
||||
│ └── vendor/
|
||||
│ └── takeone/
|
||||
│ └── components/
|
||||
│ └── widget.blade.php ← Customized cropper widget
|
||||
└── app/
|
||||
└── Http/
|
||||
└── Controllers/
|
||||
└── FamilyController.php ← Upload handler
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues:
|
||||
|
||||
1. Check the browser console (F12) for JavaScript errors
|
||||
2. Review Laravel logs: `storage/logs/laravel.log`
|
||||
3. Verify all files were modified correctly
|
||||
4. Ensure storage permissions are correct
|
||||
5. Clear all caches: `php artisan config:clear && php artisan route:clear && php artisan view:clear`
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Test the cropper functionality
|
||||
2. Upload a test image
|
||||
3. Verify it displays correctly
|
||||
4. Customize the aspect ratio if needed
|
||||
5. Add additional validation if required
|
||||
6. Consider adding image optimization/compression
|
||||
|
||||
---
|
||||
|
||||
**Installation Date**: January 26, 2026
|
||||
**Package Version**: dev-main
|
||||
**Laravel Version**: 12.0
|
||||
**Status**: ✅ Ready for Testing
|
||||
@ -158,39 +158,44 @@ class FamilyController extends Controller
|
||||
public function uploadProfilePicture(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'image' => 'required|image|mimes:jpeg,png,jpg,gif|max:5120', // 5MB max
|
||||
'image' => 'required',
|
||||
'folder' => 'required|string',
|
||||
'filename' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$user = Auth::user();
|
||||
|
||||
if ($request->hasFile('image')) {
|
||||
$image = $request->file('image');
|
||||
// Handle base64 image from cropper
|
||||
$imageData = $request->image;
|
||||
$imageParts = explode(";base64,", $imageData);
|
||||
$imageTypeAux = explode("image/", $imageParts[0]);
|
||||
$extension = $imageTypeAux[1];
|
||||
$imageBinary = base64_decode($imageParts[1]);
|
||||
|
||||
// Generate unique filename
|
||||
$filename = 'profile_' . $user->id . '_' . time() . '.' . $image->getClientOriginalExtension();
|
||||
|
||||
// Store in public/images/profiles
|
||||
$path = $image->storeAs('images/profiles', $filename, 'public');
|
||||
$folder = trim($request->folder, '/');
|
||||
$fileName = $request->filename . '.' . $extension;
|
||||
$fullPath = $folder . '/' . $fileName;
|
||||
|
||||
// Delete old profile picture if exists
|
||||
if ($user->profile_picture && \Storage::disk('public')->exists($user->profile_picture)) {
|
||||
\Storage::disk('public')->delete($user->profile_picture);
|
||||
}
|
||||
|
||||
// Update user
|
||||
$user->update(['profile_picture' => $path]);
|
||||
// Store in the public disk (storage/app/public)
|
||||
\Storage::disk('public')->put($fullPath, $imageBinary);
|
||||
|
||||
// Update user's profile_picture field
|
||||
$user->update(['profile_picture' => $fullPath]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Profile picture uploaded successfully.',
|
||||
'path' => $path,
|
||||
'path' => $fullPath,
|
||||
'url' => asset('storage/' . $fullPath)
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'No image file provided.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,11 +5,18 @@
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://git.innovator.bh/ghassan/laravel-image-cropper"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"takeone/cropper": "@dev"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
|
||||
46
composer.lock
generated
46
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d3c16cb86c42230c6c023d9a5d9bcf42",
|
||||
"content-hash": "d81f57cbd65389eb9d9702b5825564c1",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@ -5854,6 +5854,44 @@
|
||||
],
|
||||
"time": "2025-12-18T07:04:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "takeone/cropper",
|
||||
"version": "dev-main",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://git.innovator.bh/ghassan/laravel-image-cropper",
|
||||
"reference": "155876bd2165271116f7d8ea3cf3afddd9511ec9"
|
||||
},
|
||||
"require": {
|
||||
"laravel/framework": "^11.0|^12.0",
|
||||
"php": "^8.2"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Takeone\\Cropper\\CropperServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Takeone\\Cropper\\": "src/"
|
||||
}
|
||||
},
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ghassan",
|
||||
"email": "ghassan.yousif.83@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A professional image cropping component for Laravel using Bootstrap and Cropme.",
|
||||
"time": "2026-01-25T11:58:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tijsverkoyen/css-to-inline-styles",
|
||||
"version": "v2.4.0",
|
||||
@ -8434,12 +8472,14 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"stability-flags": {
|
||||
"takeone/cropper": 20
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^8.2"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
2
public/storage/.gitignore
vendored
Normal file
2
public/storage/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@ -285,15 +285,6 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Flag Icons CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@6.6.6/css/flag-icons.min.css">
|
||||
|
||||
<!-- Select2 CSS (for nationality dropdown) -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
|
||||
<!-- Flatpickr CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-box">
|
||||
<div class="card">
|
||||
@ -311,11 +302,11 @@
|
||||
|
||||
<!-- Full Name -->
|
||||
<div class="mb-3">
|
||||
<label for="full_name" class="form-label">Full Name</label>
|
||||
<input id="full_name" type="text"
|
||||
class="form-control @error('full_name') is-invalid @enderror"
|
||||
name="full_name"
|
||||
value="{{ old('full_name') }}"
|
||||
placeholder="Full Name"
|
||||
required autocomplete="name">
|
||||
@error('full_name')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@ -326,11 +317,11 @@
|
||||
|
||||
<!-- Email Address -->
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email Address</label>
|
||||
<input id="email" type="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
name="email"
|
||||
value="{{ old('email') }}"
|
||||
placeholder="Email Address"
|
||||
required autocomplete="email">
|
||||
@error('email')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@ -341,10 +332,10 @@
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input id="password" type="password"
|
||||
class="form-control @error('password') is-invalid @enderror"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required autocomplete="new-password">
|
||||
@error('password')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@ -355,15 +346,16 @@
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mb-3">
|
||||
<label for="password-confirm" class="form-label">Confirm Password</label>
|
||||
<input id="password-confirm" type="password"
|
||||
class="form-control"
|
||||
name="password_confirmation"
|
||||
placeholder="Confirm Password"
|
||||
required autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<!-- Mobile Number with Country Code -->
|
||||
<div class="mb-3">
|
||||
<label for="mobile_number" class="form-label">Mobile Number</label>
|
||||
<x-country-code-dropdown
|
||||
name="country_code"
|
||||
id="country_code"
|
||||
@ -374,7 +366,6 @@
|
||||
class="form-control @error('mobile_number') is-invalid @enderror"
|
||||
name="mobile_number"
|
||||
value="{{ old('mobile_number') }}"
|
||||
placeholder="Mobile Number"
|
||||
required autocomplete="tel">
|
||||
</x-country-code-dropdown>
|
||||
@error('mobile_number')
|
||||
@ -386,11 +377,12 @@
|
||||
|
||||
<!-- Gender -->
|
||||
<div class="mb-3">
|
||||
<label for="gender" class="form-label">Gender</label>
|
||||
<select id="gender" class="form-select @error('gender') is-invalid @enderror"
|
||||
name="gender" required>
|
||||
<option value="">Select Gender</option>
|
||||
<option value="m" {{ old('gender') == 'm' ? 'selected' : '' }}>Male</option>
|
||||
<option value="f" {{ old('gender') == 'f' ? 'selected' : '' }}>Female</option>
|
||||
<option value="m" {{ old('gender') == 'm' ? 'selected' : '' }}>♂️ Male</option>
|
||||
<option value="f" {{ old('gender') == 'f' ? 'selected' : '' }}>♀️ Female</option>
|
||||
</select>
|
||||
@error('gender')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
@ -401,15 +393,54 @@
|
||||
|
||||
<!-- Birthdate -->
|
||||
<div class="mb-3">
|
||||
<input id="birthdate" type="text"
|
||||
class="flatpickr-input @error('birthdate') is-invalid @enderror"
|
||||
name="birthdate"
|
||||
value="{{ old('birthdate') }}"
|
||||
placeholder="Birthdate"
|
||||
required
|
||||
readonly>
|
||||
<label for="birthdate" class="form-label">Birthdate</label>
|
||||
<div class="row g-2">
|
||||
<div class="col-4">
|
||||
<select id="birth_day" class="form-select @error('birthdate') is-invalid @enderror" required>
|
||||
<option value="">Day</option>
|
||||
@for($day = 1; $day <= 31; $day++)
|
||||
<option value="{{ str_pad($day, 2, '0', STR_PAD_LEFT) }}" {{ old('birth_day') == str_pad($day, 2, '0', STR_PAD_LEFT) ? 'selected' : '' }}>
|
||||
{{ $day }}
|
||||
</option>
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<select id="birth_month" class="form-select @error('birthdate') is-invalid @enderror" required>
|
||||
<option value="">Month</option>
|
||||
<option value="01" {{ old('birth_month') == '01' ? 'selected' : '' }}>January</option>
|
||||
<option value="02" {{ old('birth_month') == '02' ? 'selected' : '' }}>February</option>
|
||||
<option value="03" {{ old('birth_month') == '03' ? 'selected' : '' }}>March</option>
|
||||
<option value="04" {{ old('birth_month') == '04' ? 'selected' : '' }}>April</option>
|
||||
<option value="05" {{ old('birth_month') == '05' ? 'selected' : '' }}>May</option>
|
||||
<option value="06" {{ old('birth_month') == '06' ? 'selected' : '' }}>June</option>
|
||||
<option value="07" {{ old('birth_month') == '07' ? 'selected' : '' }}>July</option>
|
||||
<option value="08" {{ old('birth_month') == '08' ? 'selected' : '' }}>August</option>
|
||||
<option value="09" {{ old('birth_month') == '09' ? 'selected' : '' }}>September</option>
|
||||
<option value="10" {{ old('birth_month') == '10' ? 'selected' : '' }}>October</option>
|
||||
<option value="11" {{ old('birth_month') == '11' ? 'selected' : '' }}>November</option>
|
||||
<option value="12" {{ old('birth_month') == '12' ? 'selected' : '' }}>December</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<select id="birth_year" class="form-select @error('birthdate') is-invalid @enderror" required>
|
||||
<option value="">Year</option>
|
||||
@php
|
||||
$currentYear = date('Y');
|
||||
$startYear = $currentYear - 10; // Start from 10 years ago
|
||||
$endYear = 1900;
|
||||
@endphp
|
||||
@for($year = $startYear; $year >= $endYear; $year--)
|
||||
<option value="{{ $year }}" {{ old('birth_year') == $year ? 'selected' : '' }}>
|
||||
{{ $year }}
|
||||
</option>
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="birthdate" name="birthdate" value="{{ old('birthdate') }}">
|
||||
@error('birthdate')
|
||||
<span class="invalid-feedback" role="alert">
|
||||
<span class="invalid-feedback d-block" role="alert">
|
||||
<strong>{{ $message }}</strong>
|
||||
</span>
|
||||
@enderror
|
||||
@ -417,7 +448,7 @@
|
||||
|
||||
<!-- Nationality -->
|
||||
<div class="mb-3">
|
||||
<x-country-dropdown
|
||||
<x-nationality-dropdown
|
||||
name="nationality"
|
||||
id="nationality"
|
||||
:value="old('nationality')"
|
||||
@ -434,38 +465,40 @@
|
||||
<!-- /.login-box -->
|
||||
</div>
|
||||
|
||||
<!-- jQuery (required for Select2) -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<!-- Select2 JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<!-- Flatpickr JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Flatpickr for birthdate
|
||||
flatpickr('#birthdate', {
|
||||
dateFormat: 'Y-m-d',
|
||||
maxDate: 'today',
|
||||
yearRange: [1900, new Date().getFullYear()],
|
||||
disableMobile: true,
|
||||
showMonths: 1,
|
||||
clickOpens: true,
|
||||
onReady: function(selectedDates, dateStr, instance) {
|
||||
const calendar = instance.calendarContainer;
|
||||
calendar.style.fontSize = '14px';
|
||||
}
|
||||
});
|
||||
// Combine birth date dropdowns into hidden field
|
||||
const birthDay = document.getElementById('birth_day');
|
||||
const birthMonth = document.getElementById('birth_month');
|
||||
const birthYear = document.getElementById('birth_year');
|
||||
const birthdateHidden = document.getElementById('birthdate');
|
||||
|
||||
// Form submission handler
|
||||
$('#registrationForm').on('submit', function(e) {
|
||||
console.log('Form submitting...');
|
||||
console.log('Country code:', $('#country_code').val());
|
||||
console.log('Nationality:', $('#nationality').val());
|
||||
console.log('Mobile number:', $('#mobile_number').val());
|
||||
});
|
||||
function updateBirthdate() {
|
||||
const day = birthDay.value;
|
||||
const month = birthMonth.value;
|
||||
const year = birthYear.value;
|
||||
|
||||
if (day && month && year) {
|
||||
birthdateHidden.value = `${year}-${month}-${day}`;
|
||||
} else {
|
||||
birthdateHidden.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Update hidden field when any dropdown changes
|
||||
birthDay.addEventListener('change', updateBirthdate);
|
||||
birthMonth.addEventListener('change', updateBirthdate);
|
||||
birthYear.addEventListener('change', updateBirthdate);
|
||||
|
||||
// Initialize from old value if exists
|
||||
if (birthdateHidden.value) {
|
||||
const parts = birthdateHidden.value.split('-');
|
||||
if (parts.length === 3) {
|
||||
birthYear.value = parts[0];
|
||||
birthMonth.value = parts[1];
|
||||
birthDay.value = parts[2];
|
||||
}
|
||||
}
|
||||
|
||||
// Error handler
|
||||
window.onerror = function(message, source, lineno, colno, error) {
|
||||
@ -475,8 +508,16 @@
|
||||
console.error('Error:', error);
|
||||
};
|
||||
});
|
||||
|
||||
// Form submission handler
|
||||
document.getElementById('registrationForm').addEventListener('submit', function(e) {
|
||||
console.log('Form submitting...');
|
||||
console.log('Country code:', document.getElementById('country_code').value);
|
||||
console.log('Nationality:', document.getElementById('nationality').value);
|
||||
console.log('Mobile number:', document.getElementById('mobile_number').value);
|
||||
console.log('Birthdate:', document.getElementById('birthdate').value);
|
||||
});
|
||||
</script>
|
||||
|
||||
@stack('styles')
|
||||
@stack('scripts')
|
||||
@endsection
|
||||
|
||||
@ -162,8 +162,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize all country code dropdowns on the page
|
||||
document.querySelectorAll('[id$="List"]').forEach(function(listElement) {
|
||||
// Initialize only country code dropdowns (not nationality dropdowns)
|
||||
document.querySelectorAll('[id$="country_codeList"]').forEach(function(listElement) {
|
||||
const componentId = listElement.id.replace('List', '');
|
||||
initializeCountryDropdown(componentId, countries);
|
||||
});
|
||||
|
||||
@ -1,115 +1,167 @@
|
||||
@props(['name' => 'nationality', 'id' => 'nationality', 'value' => '', 'required' => false, 'error' => null])
|
||||
@props(['name' => 'nationality', 'id' => 'nationality', 'value' => '', 'required' => false, 'error' => null, 'label' => 'Nationality'])
|
||||
|
||||
<select id="{{ $id }}"
|
||||
class="form-select nationality-select @error($name) is-invalid @enderror"
|
||||
name="{{ $name }}"
|
||||
{{ $required ? 'required' : '' }}>
|
||||
<option value="">Select Nationality</option>
|
||||
</select>
|
||||
<label for="{{ $id }}" class="form-label">{{ $label }}</label>
|
||||
<div class="dropdown w-100" onclick="event.stopPropagation()">
|
||||
<button class="form-select dropdown-toggle d-flex align-items-center justify-content-between @error($name) is-invalid @enderror"
|
||||
type="button"
|
||||
id="{{ $id }}Dropdown"
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-auto-close="outside"
|
||||
aria-expanded="false"
|
||||
style="text-align: left; background-color: rgba(255,255,255,0.8);">
|
||||
<span class="d-flex align-items-center">
|
||||
<span id="{{ $id }}SelectedFlag"></span>
|
||||
<span class="country-label" id="{{ $id }}SelectedCountry">Select Nationality</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="dropdown-menu p-2 w-100" aria-labelledby="{{ $id }}Dropdown" onclick="event.stopPropagation()">
|
||||
<input type="text"
|
||||
class="form-control form-control-sm mb-2"
|
||||
placeholder="Search country..."
|
||||
id="{{ $id }}Search"
|
||||
onmousedown="event.stopPropagation()"
|
||||
onfocus="event.stopPropagation()"
|
||||
oninput="event.stopPropagation()"
|
||||
onkeydown="event.stopPropagation()"
|
||||
onkeyup="event.stopPropagation()">
|
||||
|
||||
<div class="country-list" id="{{ $id }}List" style="max-height: 300px; overflow-y: auto;">
|
||||
<!-- Countries will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="{{ $id }}" name="{{ $name }}" value="{{ $value }}" {{ $required ? 'required' : '' }}>
|
||||
</div>
|
||||
|
||||
@if($error)
|
||||
<span class="invalid-feedback" role="alert">
|
||||
<span class="invalid-feedback d-block" role="alert">
|
||||
<strong>{{ $error }}</strong>
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@once
|
||||
@push('styles')
|
||||
<style>
|
||||
.country-dropdown-btn {
|
||||
min-width: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.country-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Country data for nationality
|
||||
const countries = [
|
||||
{ name: 'United States', flagCode: 'us' },
|
||||
{ name: 'Canada', flagCode: 'ca' },
|
||||
{ name: 'United Kingdom', flagCode: 'gb' },
|
||||
{ name: 'United Arab Emirates', flagCode: 'ae' },
|
||||
{ name: 'Saudi Arabia', flagCode: 'sa' },
|
||||
{ name: 'Qatar', flagCode: 'qa' },
|
||||
{ name: 'Kuwait', flagCode: 'kw' },
|
||||
{ name: 'Bahrain', flagCode: 'bh' },
|
||||
{ name: 'Oman', flagCode: 'om' },
|
||||
{ name: 'Egypt', flagCode: 'eg' },
|
||||
{ name: 'India', flagCode: 'in' },
|
||||
{ name: 'Pakistan', flagCode: 'pk' },
|
||||
{ name: 'Bangladesh', flagCode: 'bd' },
|
||||
{ name: 'Malaysia', flagCode: 'my' },
|
||||
{ name: 'Singapore', flagCode: 'sg' },
|
||||
{ name: 'Japan', flagCode: 'jp' },
|
||||
{ name: 'China', flagCode: 'cn' },
|
||||
{ name: 'South Korea', flagCode: 'kr' },
|
||||
{ name: 'Australia', flagCode: 'au' },
|
||||
{ name: 'Germany', flagCode: 'de' },
|
||||
{ name: 'France', flagCode: 'fr' },
|
||||
{ name: 'Italy', flagCode: 'it' },
|
||||
{ name: 'Spain', flagCode: 'es' },
|
||||
{ name: 'Netherlands', flagCode: 'nl' },
|
||||
{ name: 'Sweden', flagCode: 'se' },
|
||||
{ name: 'Norway', flagCode: 'no' },
|
||||
{ name: 'Denmark', flagCode: 'dk' },
|
||||
{ name: 'Finland', flagCode: 'fi' },
|
||||
{ name: 'Switzerland', flagCode: 'ch' },
|
||||
{ name: 'Austria', flagCode: 'at' },
|
||||
{ name: 'Poland', flagCode: 'pl' },
|
||||
{ name: 'Czech Republic', flagCode: 'cz' },
|
||||
{ name: 'Hungary', flagCode: 'hu' },
|
||||
{ name: 'Romania', flagCode: 'ro' },
|
||||
{ name: 'Greece', flagCode: 'gr' },
|
||||
{ name: 'Turkey', flagCode: 'tr' },
|
||||
{ name: 'Russia', flagCode: 'ru' },
|
||||
{ name: 'Brazil', flagCode: 'br' },
|
||||
{ name: 'Mexico', flagCode: 'mx' },
|
||||
{ name: 'Argentina', flagCode: 'ar' },
|
||||
{ name: 'Chile', flagCode: 'cl' },
|
||||
{ name: 'Colombia', flagCode: 'co' },
|
||||
{ name: 'South Africa', flagCode: 'za' },
|
||||
{ name: 'Nigeria', flagCode: 'ng' },
|
||||
{ name: 'Kenya', flagCode: 'ke' },
|
||||
{ name: 'Sri Lanka', flagCode: 'lk' },
|
||||
{ name: 'Vietnam', flagCode: 'vn' },
|
||||
{ name: 'Thailand', flagCode: 'th' },
|
||||
{ name: 'Indonesia', flagCode: 'id' },
|
||||
{ name: 'Philippines', flagCode: 'ph' },
|
||||
{ name: 'New Zealand', flagCode: 'nz' },
|
||||
{ name: 'Portugal', flagCode: 'pt' },
|
||||
{ name: 'Ireland', flagCode: 'ie' },
|
||||
{ name: 'Israel', flagCode: 'il' },
|
||||
{ name: 'Jordan', flagCode: 'jo' },
|
||||
{ name: 'Lebanon', flagCode: 'lb' },
|
||||
{ name: 'Iraq', flagCode: 'iq' },
|
||||
];
|
||||
// Load countries from JSON file
|
||||
fetch('/data/countries.json')
|
||||
.then(response => response.json())
|
||||
.then(countries => {
|
||||
// Initialize only nationality dropdowns (not country code dropdowns)
|
||||
document.querySelectorAll('[id$="nationalityList"]').forEach(function(listElement) {
|
||||
const componentId = listElement.id.replace('List', '');
|
||||
initializeNationalityDropdown(componentId, countries);
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Error loading countries:', error));
|
||||
|
||||
// Initialize all nationality dropdowns on the page
|
||||
document.querySelectorAll('.nationality-select').forEach(function(selectElement) {
|
||||
if (typeof $ !== 'undefined' && $.fn.select2) {
|
||||
const $select = $(selectElement);
|
||||
function initializeNationalityDropdown(componentId, countries) {
|
||||
const countryList = document.getElementById(componentId + 'List');
|
||||
if (!countryList) return;
|
||||
|
||||
$select.select2({
|
||||
data: countries.map(country => ({
|
||||
id: country.name,
|
||||
text: country.name,
|
||||
flagCode: country.flagCode
|
||||
})),
|
||||
templateResult: function(data) {
|
||||
if (!data.id) return data.text;
|
||||
return $(`<span><span class="fi fi-${data.flagCode} me-2"></span> ${data.text}</span>`);
|
||||
},
|
||||
templateSelection: function(data) {
|
||||
if (!data.id) return data.text;
|
||||
return $(`<span><span class="fi fi-${data.flagCode} me-2"></span> ${data.text}</span>`);
|
||||
},
|
||||
placeholder: 'Select Nationality',
|
||||
allowClear: true,
|
||||
width: '100%'
|
||||
// Clear existing items
|
||||
countryList.innerHTML = '';
|
||||
|
||||
// Populate country dropdown
|
||||
countries.forEach(country => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'dropdown-item d-flex align-items-center';
|
||||
button.type = 'button';
|
||||
button.setAttribute('data-country-name', country.name);
|
||||
button.setAttribute('data-flag', country.flag);
|
||||
button.setAttribute('data-search', country.name.toLowerCase());
|
||||
|
||||
// Convert flag code to emoji
|
||||
const flagEmoji = country.iso2
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
|
||||
button.innerHTML = `
|
||||
<span class="me-2">${flagEmoji}</span>
|
||||
<span>${country.name}</span>
|
||||
`;
|
||||
button.addEventListener('click', function() {
|
||||
selectNationality(componentId, country.name, flagEmoji);
|
||||
});
|
||||
countryList.appendChild(button);
|
||||
});
|
||||
|
||||
// Restore value if provided
|
||||
const initialValue = selectElement.getAttribute('data-value') || '{{ $value }}';
|
||||
if (initialValue) {
|
||||
$select.val(initialValue).trigger('change');
|
||||
}
|
||||
// Search functionality
|
||||
const searchInput = document.getElementById(componentId + 'Search');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
const items = countryList.querySelectorAll('.dropdown-item');
|
||||
items.forEach(item => {
|
||||
const searchText = item.getAttribute('data-search') || '';
|
||||
if (searchText.includes(searchTerm)) {
|
||||
item.classList.remove('d-none');
|
||||
} else {
|
||||
item.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Set initial value if provided
|
||||
const hiddenInput = document.getElementById(componentId);
|
||||
if (hiddenInput && hiddenInput.value) {
|
||||
const initialCountry = countries.find(c => c.name === hiddenInput.value);
|
||||
if (initialCountry) {
|
||||
const flagEmoji = initialCountry.iso2
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('');
|
||||
selectNationality(componentId, initialCountry.name, flagEmoji);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectNationality(componentId, name, flag) {
|
||||
const flagElement = document.getElementById(componentId + 'SelectedFlag');
|
||||
const countryElement = document.getElementById(componentId + 'SelectedCountry');
|
||||
const hiddenInput = document.getElementById(componentId);
|
||||
|
||||
if (flagElement) flagElement.textContent = flag + ' ';
|
||||
if (countryElement) countryElement.textContent = name;
|
||||
if (hiddenInput) hiddenInput.value = name;
|
||||
|
||||
// Close the dropdown after selection
|
||||
const dropdownButton = document.getElementById(componentId + 'Dropdown');
|
||||
if (dropdownButton) {
|
||||
const dropdown = bootstrap.Dropdown.getInstance(dropdownButton);
|
||||
if (dropdown) dropdown.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
|
||||
@ -12,14 +12,40 @@
|
||||
<!-- Profile Picture Section -->
|
||||
<div class="mb-4 text-center">
|
||||
<div class="mb-3">
|
||||
<img src="{{ $user->profile_picture ? asset('storage/' . $user->profile_picture) : asset('images/default-avatar.png') }}"
|
||||
@if($user->profile_picture && file_exists(public_path('storage/' . $user->profile_picture)))
|
||||
<img src="{{ asset('storage/' . $user->profile_picture) }}"
|
||||
alt="Profile Picture"
|
||||
class="rounded-circle"
|
||||
style="width: 120px; height: 120px; object-fit: cover; border: 3px solid #dee2e6;">
|
||||
style="width: 300px; height: 400px; object-fit: cover; border: 3px solid #dee2e6; border-radius: 8px;">
|
||||
@elseif(file_exists(public_path('storage/images/profiles/profile_' . $user->id . '.png')))
|
||||
<img src="{{ asset('storage/images/profiles/profile_' . $user->id . '.png') }}"
|
||||
alt="Profile Picture"
|
||||
style="width: 300px; height: 400px; object-fit: cover; border: 3px solid #dee2e6; border-radius: 8px;">
|
||||
@elseif(file_exists(public_path('storage/images/profiles/profile_' . $user->id . '.jpg')))
|
||||
<img src="{{ asset('storage/images/profiles/profile_' . $user->id . '.jpg') }}"
|
||||
alt="Profile Picture"
|
||||
style="width: 300px; height: 400px; object-fit: cover; border: 3px solid #dee2e6; border-radius: 8px;">
|
||||
@elseif(file_exists(public_path('storage/images/profiles/profile_' . $user->id . '.jpeg')))
|
||||
<img src="{{ asset('storage/images/profiles/profile_' . $user->id . '.jpeg') }}"
|
||||
alt="Profile Picture"
|
||||
style="width: 300px; height: 400px; object-fit: cover; border: 3px solid #dee2e6; border-radius: 8px;">
|
||||
@else
|
||||
<div style="width: 300px; height: 400px; background-color: #f0f0f0; border: 3px solid #dee2e6; border-radius: 8px; display: flex; align-items: center; justify-content: center; margin: 0 auto;">
|
||||
<div class="text-center">
|
||||
<i class="bi bi-person-circle" style="font-size: 100px; color: #dee2e6;"></i>
|
||||
<p class="text-muted mt-2">No profile picture</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#profilePictureModal">
|
||||
<i class="fas fa-camera"></i> Change Profile Picture
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<x-takeone-cropper
|
||||
id="profile_picture"
|
||||
width="300"
|
||||
height="400"
|
||||
shape="square"
|
||||
folder="images/profiles"
|
||||
filename="profile_{{ $user->id }}"
|
||||
uploadUrl="{{ route('profile.upload-picture') }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('profile.update') }}">
|
||||
@ -196,14 +222,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Picture Upload Modal -->
|
||||
<x-image-upload-modal
|
||||
id="profilePictureModal"
|
||||
aspectRatio="1"
|
||||
maxSizeMB="1"
|
||||
title="Upload Profile Picture"
|
||||
uploadUrl="{{ route('profile.upload-picture') }}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
|
||||
@ -42,8 +42,8 @@
|
||||
<div class="d-flex">
|
||||
<!-- Profile Picture -->
|
||||
<div style="width: 180px; min-height: 250px; border-radius: 0.375rem 0 0 0.375rem;">
|
||||
@if($relationship->dependent->media_gallery[0] ?? false)
|
||||
<img src="{{ $relationship->dependent->media_gallery[0] }}" alt="{{ $relationship->dependent->full_name }}" class="w-100 h-100" style="object-fit: cover; border-radius: 0.375rem 0 0 0.375rem;">
|
||||
@if($relationship->dependent->profile_picture)
|
||||
<img src="{{ asset('storage/' . $relationship->dependent->profile_picture) }}" alt="{{ $relationship->dependent->full_name }}" class="w-100 h-100" style="object-fit: cover; border-radius: 0.375rem 0 0 0.375rem;">
|
||||
@else
|
||||
<div class="w-100 h-100 d-flex align-items-center justify-content-center text-white fw-bold" style="font-size: 3rem; background: linear-gradient(135deg, {{ $relationship->dependent->gender == 'm' ? '#0d6efd 0%, #0a58ca 100%' : '#d63384 0%, #a61e4d 100%' }}); border-radius: 0.375rem 0 0 0.375rem;">
|
||||
{{ strtoupper(substr($relationship->dependent->full_name, 0, 1)) }}
|
||||
|
||||
@ -447,5 +447,8 @@
|
||||
@vite(['resources/js/app.js'])
|
||||
|
||||
@stack('scripts')
|
||||
|
||||
<!-- Modals Stack (for cropper and other modals) -->
|
||||
@stack('modals')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
156
resources/views/vendor/takeone/components/widget.blade.php
vendored
Normal file
156
resources/views/vendor/takeone/components/widget.blade.php
vendored
Normal file
@ -0,0 +1,156 @@
|
||||
@php
|
||||
$id = $attributes->get('id');
|
||||
$width = $attributes->get('width', 300);
|
||||
$height = $attributes->get('height', 300);
|
||||
$shape = $attributes->get('shape', 'circle');
|
||||
$folder = $attributes->get('folder', 'uploads');
|
||||
$filename = $attributes->get('filename', 'cropped_' . time());
|
||||
$uploadUrl = $attributes->get('uploadUrl', route('profile.upload-picture'));
|
||||
@endphp
|
||||
|
||||
@once
|
||||
<link rel="stylesheet" href="https://unpkg.com/cropme@1.4.1/dist/cropme.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://unpkg.com/cropme@1.4.1/dist/cropme.min.js"></script>
|
||||
<style>
|
||||
.modal-content-clean { border: none; border-radius: 15px; overflow: hidden; }
|
||||
.cropme-wrapper { overflow: hidden !important; border-radius: 8px; }
|
||||
.cropme-slider { display: none !important; }
|
||||
.takeone-canvas {
|
||||
height: 400px;
|
||||
background: #111;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
border: 1px solid #222;
|
||||
}
|
||||
.custom-slider-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: #6c757d;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.form-range::-webkit-slider-thumb { background: #198754; }
|
||||
.form-range::-moz-range-thumb { background: #198754; }
|
||||
</style>
|
||||
@endonce
|
||||
|
||||
<button type="button" class="btn btn-success px-4 fw-bold shadow-sm" data-bs-toggle="modal" data-bs-target="#cropperModal_{{ $id }}">
|
||||
Change Photo
|
||||
</button>
|
||||
|
||||
@push('modals')
|
||||
<div class="modal fade" id="cropperModal_{{ $id }}" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content modal-content-clean shadow-lg">
|
||||
<div class="modal-body p-4 text-start">
|
||||
<div class="mb-3 d-flex align-items-center">
|
||||
<input type="file" id="input_{{ $id }}" class="form-control form-control-sm" accept="image/*">
|
||||
<button type="button" class="btn-close ms-2" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div id="box_{{ $id }}" class="takeone-canvas"></div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="custom-slider-label d-block mb-2">Zoom Level</label>
|
||||
<input type="range" class="form-range" id="zoom_{{ $id }}" min="0" max="100" step="1" value="0">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="custom-slider-label d-block mb-2">Rotation</label>
|
||||
<input type="range" class="form-range" id="rot_{{ $id }}" min="-180" max="180" step="1" value="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 mt-2">
|
||||
<button type="button" class="btn btn-success btn-lg fw-bold py-3" id="save_{{ $id }}">
|
||||
Crop & Save Image
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
let cropper_{{ $id }} = null;
|
||||
const el_{{ $id }} = document.getElementById("box_{{ $id }}");
|
||||
const zoomMin_{{ $id }} = 0.01;
|
||||
const zoomMax_{{ $id }} = 3;
|
||||
|
||||
function applyTransform_{{ $id }}(instance) {
|
||||
if (!instance.properties.image) return;
|
||||
const p = instance.properties;
|
||||
const t = `translate3d(${p.x}px, ${p.y}px, 0) scale(${p.scale}) rotate(${p.deg}deg)`;
|
||||
p.image.style.transform = t;
|
||||
}
|
||||
|
||||
$('#input_{{ $id }}').on('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
if (cropper_{{ $id }}) cropper_{{ $id }}.destroy();
|
||||
|
||||
cropper_{{ $id }} = new Cropme(el_{{ $id }}, {
|
||||
container: { width: '100%', height: 400 },
|
||||
viewport: {
|
||||
width: {{ $width }},
|
||||
height: {{ $height }},
|
||||
type: '{{ $shape }}',
|
||||
border: { enable: true, width: 2, color: '#fff' }
|
||||
},
|
||||
transformOrigin: 'viewport',
|
||||
zoom: { min: zoomMin_{{ $id }}, max: zoomMax_{{ $id }}, enable: true, mouseWheel: true, slider: false },
|
||||
rotation: { enable: true, slider: false }
|
||||
});
|
||||
|
||||
cropper_{{ $id }}.bind({ url: event.target.result }).then(() => {
|
||||
$('#zoom_{{ $id }}').val(0);
|
||||
$('#rot_{{ $id }}').val(0);
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
$('#zoom_{{ $id }}').on('input', function() {
|
||||
if (!cropper_{{ $id }} || !cropper_{{ $id }}.properties.image) return;
|
||||
const p = parseFloat($(this).val());
|
||||
const scale = zoomMin_{{ $id }} + (zoomMax_{{ $id }} - zoomMin_{{ $id }}) * (p / 100);
|
||||
cropper_{{ $id }}.properties.scale = Math.min(Math.max(scale, zoomMin_{{ $id }}), zoomMax_{{ $id }});
|
||||
applyTransform_{{ $id }}(cropper_{{ $id }});
|
||||
});
|
||||
|
||||
$('#rot_{{ $id }}').on('input', function() {
|
||||
if (cropper_{{ $id }}) {
|
||||
cropper_{{ $id }}.rotate(parseInt($(this).val(), 10));
|
||||
}
|
||||
});
|
||||
|
||||
$('#save_{{ $id }}').on('click', function() {
|
||||
if (!cropper_{{ $id }}) return;
|
||||
const btn = $(this);
|
||||
btn.prop('disabled', true).text('Uploading...');
|
||||
|
||||
cropper_{{ $id }}.crop({ type: 'base64' }).then(base64 => {
|
||||
$.post("{{ $uploadUrl }}", {
|
||||
_token: "{{ csrf_token() }}",
|
||||
image: base64,
|
||||
folder: '{{ $folder }}',
|
||||
filename: '{{ $filename }}'
|
||||
}).done((res) => {
|
||||
alert('Saved successfully!');
|
||||
$('#cropperModal_{{ $id }}').modal('hide');
|
||||
// Reload page to show new image
|
||||
location.reload();
|
||||
}).fail((err) => {
|
||||
alert('Upload failed: ' + (err.responseJSON?.message || 'Unknown error'));
|
||||
}).always(() => {
|
||||
btn.prop('disabled', false).text('Crop & Save Image');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
Loading…
x
Reference in New Issue
Block a user