Added capture photo logic in livewire
Some checks failed
Gemini PR Review / Gemini PR Review (pull_request) Waiting to run
Scan for leaked secrets using Kingfisher / kingfisher-secrets-scan (pull_request) Waiting to run
Laravel Larastan / larastan (pull_request) Waiting to run
Laravel Pint / pint (pull_request) Waiting to run
Scan for leaked secrets using Kingfisher / kingfisher-secrets-scan (push) Has been cancelled
Some checks failed
Gemini PR Review / Gemini PR Review (pull_request) Waiting to run
Scan for leaked secrets using Kingfisher / kingfisher-secrets-scan (pull_request) Waiting to run
Laravel Larastan / larastan (pull_request) Waiting to run
Laravel Pint / pint (pull_request) Waiting to run
Scan for leaked secrets using Kingfisher / kingfisher-secrets-scan (push) Has been cancelled
This commit is contained in:
32
app/Livewire/Webcam.php
Normal file
32
app/Livewire/Webcam.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Webcam extends Component
|
||||
{
|
||||
|
||||
public string $capturedPhoto = '';
|
||||
|
||||
// Called from JavaScript when a photo is taken
|
||||
public function setPhoto(string $photo): void
|
||||
{
|
||||
$this->capturedPhoto = $photo;
|
||||
|
||||
// Fires a browser event that the Filament form will listen to
|
||||
$this->dispatch('photo-captured', photo: $photo);
|
||||
}
|
||||
|
||||
// Called from JavaScript when user clicks "Retake"
|
||||
public function clearPhoto(): void
|
||||
{
|
||||
$this->capturedPhoto = '';
|
||||
|
||||
$this->dispatch('photo-captured', photo: '');
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.webcam');
|
||||
}
|
||||
}
|
||||
10
resources/views/components/webcam-field.blade.php
Normal file
10
resources/views/components/webcam-field.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<div>
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">
|
||||
Visitor Photo
|
||||
</x-slot>
|
||||
|
||||
<livewire:webcam-capture />
|
||||
|
||||
</x-filament::section>
|
||||
</div>
|
||||
189
resources/views/livewire/webcam.blade.php
Normal file
189
resources/views/livewire/webcam.blade.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<div
|
||||
x-data="{
|
||||
cameraActive: false,
|
||||
captured: false,
|
||||
errorMessage: '',
|
||||
|
||||
async startCamera() {
|
||||
this.errorMessage = '';
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: 640, height: 480, facingMode: 'user' }
|
||||
});
|
||||
this.$refs.video.srcObject = stream;
|
||||
await this.$refs.video.play();
|
||||
this.cameraActive = true;
|
||||
} catch (err) {
|
||||
if (err.name === 'NotAllowedError') {
|
||||
this.errorMessage = 'Camera permission denied. Please allow camera access in your browser and try again.';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
this.errorMessage = 'No camera found. Please connect a webcam and try again.';
|
||||
} else {
|
||||
this.errorMessage = 'Could not access camera: ' + err.message;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
capture() {
|
||||
const canvas = this.$refs.canvas;
|
||||
const video = this.$refs.video;
|
||||
canvas.width = video.videoWidth || 640;
|
||||
canvas.height = video.videoHeight || 480;
|
||||
canvas.getContext('2d').drawImage(video, 0, 0);
|
||||
const photoData = canvas.toDataURL('image/jpeg', 0.85);
|
||||
|
||||
// Stop the camera stream after capture
|
||||
if (video.srcObject) {
|
||||
video.srcObject.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
this.cameraActive = false;
|
||||
this.captured = true;
|
||||
|
||||
// Send photo data to PHP via Livewire
|
||||
$wire.setPhoto(photoData);
|
||||
},
|
||||
|
||||
retake() {
|
||||
this.captured = false;
|
||||
$wire.clearPhoto();
|
||||
this.$nextTick(() => this.startCamera());
|
||||
}
|
||||
}"
|
||||
style="font-family: inherit;"
|
||||
>
|
||||
{{-- ── Error message ── --}}
|
||||
<template x-if="errorMessage">
|
||||
<div style="
|
||||
background: #3b1a1a;
|
||||
border: 1px solid #7f2020;
|
||||
color: #f87171;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
" x-text="errorMessage"></div>
|
||||
</template>
|
||||
|
||||
{{-- ── Live video feed (shown while camera is active) ── --}}
|
||||
<div x-show="cameraActive && !captured" style="position: relative;">
|
||||
<video
|
||||
x-ref="video"
|
||||
autoplay
|
||||
playsinline
|
||||
muted
|
||||
style="
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
border-radius: 10px;
|
||||
display: block;
|
||||
background: #111;
|
||||
"
|
||||
></video>
|
||||
</div>
|
||||
|
||||
{{-- ── Captured photo preview (shown after capture) ── --}}
|
||||
<div x-show="captured" style="position: relative;">
|
||||
<img
|
||||
x-bind:src="$refs.canvas ? $refs.canvas.toDataURL('image/jpeg') : ''"
|
||||
style="
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
border-radius: 10px;
|
||||
display: block;
|
||||
border: 2px solid #16a34a;
|
||||
"
|
||||
alt="Captured visitor photo"
|
||||
/>
|
||||
<div style="
|
||||
position: absolute;
|
||||
top: 10px; left: 10px;
|
||||
background: #16a34a;
|
||||
color: white;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
">✓ Photo captured</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Placeholder (before camera starts) ── --}}
|
||||
<div
|
||||
x-show="!cameraActive && !captured"
|
||||
style="
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
height: 200px;
|
||||
background: #1a1a1a;
|
||||
border: 2px dashed #444;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
📷 Camera not started yet
|
||||
</div>
|
||||
|
||||
{{-- ── Hidden canvas used for capturing the frame ── --}}
|
||||
<canvas x-ref="canvas" style="display: none;"></canvas>
|
||||
|
||||
{{-- ── Buttons ── --}}
|
||||
<div style="display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap;">
|
||||
|
||||
{{-- Start Camera button --}}
|
||||
<button
|
||||
type="button"
|
||||
x-show="!cameraActive && !captured"
|
||||
x-on:click="startCamera()"
|
||||
style="
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 9px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
📷 Start Camera
|
||||
</button>
|
||||
|
||||
{{-- Capture button --}}
|
||||
<button
|
||||
type="button"
|
||||
x-show="cameraActive && !captured"
|
||||
x-on:click="capture()"
|
||||
style="
|
||||
background: #16a34a;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 9px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
📸 Capture Photo
|
||||
</button>
|
||||
|
||||
{{-- Retake button --}}
|
||||
<button
|
||||
type="button"
|
||||
x-show="captured"
|
||||
x-on:click="retake()"
|
||||
style="
|
||||
background: #b45309;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 9px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
"
|
||||
>
|
||||
🔄 Retake Photo
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user