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
190 lines
5.7 KiB
PHP
190 lines
5.7 KiB
PHP
<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>
|