From 80deff661d677cc21bad85b0c78141b045a67dc3 Mon Sep 17 00:00:00 2001 From: Yutaka Kurosaki Date: Sat, 13 Dec 2025 20:08:40 +0900 Subject: [PATCH] Add image upload support to document editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../Controllers/ImageUploadController.php | 54 +++++++++++++++++++ .../views/livewire/document-editor.blade.php | 36 +++++++++++++ src/routes/web.php | 4 ++ 3 files changed, 94 insertions(+) create mode 100644 src/app/Http/Controllers/ImageUploadController.php diff --git a/src/app/Http/Controllers/ImageUploadController.php b/src/app/Http/Controllers/ImageUploadController.php new file mode 100644 index 0000000..822f9a5 --- /dev/null +++ b/src/app/Http/Controllers/ImageUploadController.php @@ -0,0 +1,54 @@ +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, + ], + ]); + } +} diff --git a/src/resources/views/livewire/document-editor.blade.php b/src/resources/views/livewire/document-editor.blade.php index eece6f5..5b1bbe9 100644 --- a/src/resources/views/livewire/document-editor.blade.php +++ b/src/resources/views/livewire/document-editor.blade.php @@ -147,6 +147,42 @@ class="w-full" 'guide' ], 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 = `![${altText}](${url})`; + cm.replaceSelection(markdown); + }) + .catch(error => { + onError(error.message || 'Failed to upload image'); + }); + }, }); this.editor.codemirror.on('change', () => { diff --git a/src/routes/web.php b/src/routes/web.php index 86afa5e..afdbd37 100644 --- a/src/routes/web.php +++ b/src/routes/web.php @@ -2,6 +2,7 @@ use App\Http\Controllers\ProfileController; use App\Http\Controllers\LocaleController; +use App\Http\Controllers\ImageUploadController; use App\Http\Controllers\Admin\UserController as AdminUserController; use App\Livewire\DocumentViewer; use App\Livewire\DocumentEditor; @@ -29,6 +30,9 @@ Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); 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