Added webcam livewire pages
Some checks failed
Scan for leaked secrets using Kingfisher / kingfisher-secrets-scan (push) Has been cancelled
Some checks failed
Scan for leaked secrets using Kingfisher / kingfisher-secrets-scan (push) Has been cancelled
This commit is contained in:
33
app/Livewire/Webcam.php
Normal file
33
app/Livewire/Webcam.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
193
resources/views/livewire/webcam.blade.php
Normal file
193
resources/views/livewire/webcam.blade.php
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<div
|
||||||
|
x-data="{
|
||||||
|
cameraActive: false,
|
||||||
|
captured: false,
|
||||||
|
errorMessage: '',
|
||||||
|
photoData: '',
|
||||||
|
|
||||||
|
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;
|
||||||
|
this.photoData = photoData;
|
||||||
|
|
||||||
|
// Send photo data to PHP via Livewire
|
||||||
|
$wire.setPhoto(photoData);
|
||||||
|
},
|
||||||
|
|
||||||
|
retake() {
|
||||||
|
this.captured = false;
|
||||||
|
this.photoData = '';
|
||||||
|
$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') : ''" --}}
|
||||||
|
x-bind:src="photoData"
|
||||||
|
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