add views and components
This commit is contained in:
parent
380742ad77
commit
079f65bc5f
11
app/Http/Controllers/PhotoController.php
Normal file
11
app/Http/Controllers/PhotoController.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
class PhotoController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('camera');
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/Livewire/CameraCapture.php
Normal file
45
app/Livewire/CameraCapture.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Models\PhotoJob;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class CameraCapture extends Component
|
||||||
|
{
|
||||||
|
public string $status = '';
|
||||||
|
|
||||||
|
public function capture(string $imageData): void
|
||||||
|
{
|
||||||
|
if (!preg_match('/^data:image\/(\w+);base64,/', $imageData, $type)) {
|
||||||
|
$this->status = 'Error: Invalid image data';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = strtolower($type[1]);
|
||||||
|
$filename = 'photos/' . Str::uuid() . '.' . $extension;
|
||||||
|
$raw = base64_decode(substr($imageData, strpos($imageData, ',') + 1));
|
||||||
|
|
||||||
|
Storage::disk('public')->put($filename, $raw);
|
||||||
|
|
||||||
|
$job = PhotoJob::create([
|
||||||
|
'image_path' => $filename,
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->status = "Job #{$job->id} created";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$recentPhotos = PhotoJob::latest()->take(10)->get()->map(fn($job) => [
|
||||||
|
'id' => $job->id,
|
||||||
|
'url' => Storage::disk('public')->url($job->image_path),
|
||||||
|
'status' => $job->status,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('livewire.camera-capture', compact('recentPhotos'));
|
||||||
|
}
|
||||||
|
}
|
||||||
20
resources/views/camera.blade.php
Normal file
20
resources/views/camera.blade.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Fotobox</title>
|
||||||
|
@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
@endif
|
||||||
|
@livewireStyles
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-950 text-white min-h-screen flex flex-col items-center justify-center p-6">
|
||||||
|
|
||||||
|
<h1 class="text-3xl font-bold mb-8 tracking-wide">Fotobox</h1>
|
||||||
|
|
||||||
|
@livewire('camera-capture')
|
||||||
|
|
||||||
|
@livewireScripts
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
75
resources/views/livewire/camera-capture.blade.php
Normal file
75
resources/views/livewire/camera-capture.blade.php
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<div class="flex flex-col items-center w-full">
|
||||||
|
<div class="relative w-full max-w-2xl">
|
||||||
|
<video id="video" autoplay playsinline
|
||||||
|
class="w-full rounded-2xl shadow-2xl bg-gray-900 aspect-video object-cover"></video>
|
||||||
|
|
||||||
|
<canvas id="canvas" class="hidden"></canvas>
|
||||||
|
|
||||||
|
<div id="flash" class="absolute inset-0 rounded-2xl bg-white opacity-0 pointer-events-none transition-opacity duration-150"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="capture-btn"
|
||||||
|
wire:loading.attr="disabled"
|
||||||
|
class="mt-8 px-10 py-4 bg-white text-gray-950 font-semibold text-lg rounded-full shadow-lg hover:bg-gray-200 active:scale-95 transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
<span wire:loading.remove>Take Photo</span>
|
||||||
|
<span wire:loading>Saving…</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mt-4 text-sm text-gray-400 h-6">{{ $status }}</div>
|
||||||
|
|
||||||
|
@if ($recentPhotos->isNotEmpty())
|
||||||
|
<div class="mt-10 w-full max-w-2xl">
|
||||||
|
<h2 class="text-sm font-medium text-gray-400 mb-3 tracking-wide uppercase">Recent Photos</h2>
|
||||||
|
<div class="grid grid-cols-5 gap-2">
|
||||||
|
@foreach ($recentPhotos as $photo)
|
||||||
|
<div class="relative group">
|
||||||
|
<img src="{{ $photo['url'] }}" alt="Photo #{{ $photo['id'] }}"
|
||||||
|
class="w-full aspect-square object-cover rounded-lg border border-gray-700">
|
||||||
|
<span class="absolute bottom-1 left-1 text-[10px] px-1.5 py-0.5 rounded bg-black/60 text-yellow-400">
|
||||||
|
{{ $photo['status'] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@script
|
||||||
|
<script>
|
||||||
|
const video = document.getElementById('video');
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const btn = document.getElementById('capture-btn');
|
||||||
|
const flash = document.getElementById('flash');
|
||||||
|
|
||||||
|
async function startCamera() {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
|
||||||
|
video.srcObject = stream;
|
||||||
|
} catch {
|
||||||
|
$wire.set('status', 'Camera access denied or unavailable.');
|
||||||
|
btn.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerFlash() {
|
||||||
|
flash.style.opacity = '0.8';
|
||||||
|
setTimeout(() => flash.style.opacity = '0', 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
if (!video.srcObject) return;
|
||||||
|
|
||||||
|
triggerFlash();
|
||||||
|
|
||||||
|
canvas.width = video.videoWidth;
|
||||||
|
canvas.height = video.videoHeight;
|
||||||
|
canvas.getContext('2d').drawImage(video, 0, 0);
|
||||||
|
const imageData = canvas.toDataURL('image/jpeg', 0.9);
|
||||||
|
|
||||||
|
await $wire.capture(imageData);
|
||||||
|
});
|
||||||
|
|
||||||
|
startCamera();
|
||||||
|
</script>
|
||||||
|
@endscript
|
||||||
|
</div>
|
||||||
@ -1,7 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\PhotoController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', [PhotoController::class, 'index'])->name('camera');
|
||||||
return view('welcome');
|
|
||||||
});
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user