Add image upload support to document editor
- Create ImageUploadController to handle image uploads - Store images in storage/app/public/images with UUID filenames - Integrate with EasyMDE editor for drag-drop, paste, and toolbar upload - Use original filename as alt text in markdown 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
54
src/app/Http/Controllers/ImageUploadController.php
Normal file
54
src/app/Http/Controllers/ImageUploadController.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ImageUploadController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle image upload from EasyMDE editor
|
||||||
|
*/
|
||||||
|
public function upload(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'image' => [
|
||||||
|
'required',
|
||||||
|
'file',
|
||||||
|
'mimes:jpeg,jpg,png,gif,webp',
|
||||||
|
'max:2048', // 2MB
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$file = $request->file('image');
|
||||||
|
|
||||||
|
// Get original filename without extension for alt text
|
||||||
|
$originalName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
|
||||||
|
|
||||||
|
// Generate unique filename: YYYY/MM/uuid.extension
|
||||||
|
$year = date('Y');
|
||||||
|
$month = date('m');
|
||||||
|
$extension = $file->getClientOriginalExtension();
|
||||||
|
$filename = Str::uuid() . '.' . $extension;
|
||||||
|
$path = "images/{$year}/{$month}/{$filename}";
|
||||||
|
|
||||||
|
// Store to public disk
|
||||||
|
Storage::disk('public')->putFileAs(
|
||||||
|
"images/{$year}/{$month}",
|
||||||
|
$file,
|
||||||
|
$filename
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return URL for EasyMDE (use APP_URL)
|
||||||
|
$url = asset('storage/' . $path);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => [
|
||||||
|
'filePath' => $url,
|
||||||
|
'altText' => $originalName,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -147,6 +147,42 @@ class="w-full"
|
|||||||
'guide'
|
'guide'
|
||||||
],
|
],
|
||||||
status: ['lines', 'words', 'cursor'],
|
status: ['lines', 'words', 'cursor'],
|
||||||
|
// Image upload configuration
|
||||||
|
uploadImage: true,
|
||||||
|
imageMaxSize: 2 * 1024 * 1024, // 2MB
|
||||||
|
imageAccept: 'image/png, image/jpeg, image/gif, image/webp',
|
||||||
|
imageUploadFunction: (file, onSuccess, onError) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('image', file);
|
||||||
|
|
||||||
|
fetch('{{ route("images.upload") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return response.json().then(data => {
|
||||||
|
throw new Error(data.message || 'Upload failed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Insert markdown with alt text directly
|
||||||
|
const cm = this.editor.codemirror;
|
||||||
|
const altText = data.data.altText || 'image';
|
||||||
|
const url = data.data.filePath;
|
||||||
|
const markdown = ``;
|
||||||
|
cm.replaceSelection(markdown);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
onError(error.message || 'Failed to upload image');
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editor.codemirror.on('change', () => {
|
this.editor.codemirror.on('change', () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
use App\Http\Controllers\LocaleController;
|
use App\Http\Controllers\LocaleController;
|
||||||
|
use App\Http\Controllers\ImageUploadController;
|
||||||
use App\Http\Controllers\Admin\UserController as AdminUserController;
|
use App\Http\Controllers\Admin\UserController as AdminUserController;
|
||||||
use App\Livewire\DocumentViewer;
|
use App\Livewire\DocumentViewer;
|
||||||
use App\Livewire\DocumentEditor;
|
use App\Livewire\DocumentEditor;
|
||||||
@@ -29,6 +30,9 @@
|
|||||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||||
|
|
||||||
|
// Image upload for editor
|
||||||
|
Route::post('/images/upload', [ImageUploadController::class, 'upload'])->name('images.upload');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin routes
|
// Admin routes
|
||||||
|
|||||||
Reference in New Issue
Block a user