diff --git a/INSTALLATION_SUMMARY.md b/INSTALLATION_SUMMARY.md new file mode 100644 index 0000000..af015e0 --- /dev/null +++ b/INSTALLATION_SUMMARY.md @@ -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 `` tag +- This allows the cropper modal to be injected into the page + +### 3. resources/views/family/profile-edit.blade.php +- Replaced the old `` component with `` +- 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` diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md new file mode 100644 index 0000000..f40a0d2 --- /dev/null +++ b/TESTING_GUIDE.md @@ -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 `` 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 + + + + + + + + + + + +``` + +### Change to Circular Crop + +```html + +``` + +### Change Storage Folder + +```html + +``` + +### Custom Filename Pattern + +```html + +``` + +## 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 diff --git a/app/Http/Controllers/FamilyController.php b/app/Http/Controllers/FamilyController.php index 992d586..f4421b5 100644 --- a/app/Http/Controllers/FamilyController.php +++ b/app/Http/Controllers/FamilyController.php @@ -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', ]); - $user = Auth::user(); + 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); } /** diff --git a/composer.json b/composer.json index 6ede6a4..1769728 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index b41b38b..aebd892 100644 --- a/composer.lock +++ b/composer.lock @@ -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" } diff --git a/public/storage/.gitignore b/public/storage/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/public/storage/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index ef7d6e8..8c765a3 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -285,15 +285,6 @@ } - - - - - - - - -