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:
2025-12-13 20:08:40 +09:00
parent 8ea8b3f6b6
commit 80deff661d
3 changed files with 94 additions and 0 deletions

View 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,
],
]);
}
}

View File

@@ -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 = `![${altText}](${url})`;
cm.replaceSelection(markdown);
})
.catch(error => {
onError(error.message || 'Failed to upload image');
});
},
}); });
this.editor.codemirror.on('change', () => { this.editor.codemirror.on('change', () => {

View File

@@ -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