deckhearth/src/components/CameraScanner.tsx
Randall Stillwell dc95d0d76d Implement complete user authentication and admin panel system
 Features Added:
- User registration and login with JWT authentication
- Role-based access control (user/admin)
- Comprehensive admin panel with user management
- Password hashing with bcrypt
- Session management and token storage
- Protected routes and public routes
- Modern login/register forms with validation

🗄️ Database Schema:
- Users table with profile information
- Roles and permissions system
- User-role junction tables
- Session management tables
- Database triggers and indexes

🎨 UI/UX Improvements:
- Updated navigation with user menu
- Admin badge and access controls
- Responsive authentication forms
- Loading states and error handling
- Role-based UI elements

🔧 API Endpoints:
- /api/auth/login - User authentication
- /api/auth/register - User registration
- /api/admin/users - User management (admin only)
- /api/setup-auth - Database schema setup

🚀 Admin Panel Features:
- User listing with search and pagination
- Role assignment (user/admin)
- User activation/deactivation
- System dashboard with stats
- Card management placeholder
- Real-time user management
2025-07-21 21:06:38 -05:00

319 lines
No EOL
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useRef, useEffect } from 'react';
import Tesseract from 'tesseract.js';
interface CameraScannerProps {
onCardScanned: (cardData: any) => void;
onError: (error: string) => void;
}
interface ScanResult {
text: string;
confidence: number;
cardName?: string;
setName?: string;
}
const CameraScanner: React.FC<CameraScannerProps> = ({ onCardScanned, onError }) => {
const [isStreaming, setIsStreaming] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [scanResult, setScanResult] = useState<ScanResult | null>(null);
const [capturedImage, setCapturedImage] = useState<string | null>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const streamRef = useRef<MediaStream | null>(null);
// Start camera stream
const startCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment', // Use back camera on mobile
width: { ideal: 1920 },
height: { ideal: 1080 }
}
});
if (videoRef.current) {
videoRef.current.srcObject = stream;
streamRef.current = stream;
setIsStreaming(true);
}
} catch (err) {
console.error('Camera access error:', err);
onError('Unable to access camera. Please check permissions.');
}
};
// Stop camera stream
const stopCamera = () => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
setIsStreaming(false);
setCapturedImage(null);
setScanResult(null);
};
// Capture image from video stream
const captureImage = () => {
if (!videoRef.current || !canvasRef.current) return;
const video = videoRef.current;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
// Set canvas dimensions to match video
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// Draw current video frame to canvas
ctx?.drawImage(video, 0, 0, canvas.width, canvas.height);
// Get image data URL
const imageDataUrl = canvas.toDataURL('image/jpeg', 0.8);
setCapturedImage(imageDataUrl);
// Process with OCR
processImage(imageDataUrl);
};
// Process image with Tesseract OCR
const processImage = async (imageData: string) => {
setIsProcessing(true);
setScanResult(null);
try {
const { data: { text, confidence } } = await Tesseract.recognize(imageData, 'eng', {
logger: m => console.log(m) // Optional: log progress
});
console.log('OCR Result:', text);
console.log('Confidence:', confidence);
// Extract card information from OCR text
const cardInfo = extractCardInfo(text);
const result: ScanResult = {
text: text.trim(),
confidence,
cardName: cardInfo.name,
setName: cardInfo.set
};
setScanResult(result);
// If we found card info, try to match against database
if (cardInfo.name) {
onCardScanned({
name: cardInfo.name,
set: cardInfo.set,
ocrText: text,
confidence: confidence
});
}
} catch (error) {
console.error('OCR processing error:', error);
onError('Failed to process image. Please try again.');
} finally {
setIsProcessing(false);
}
};
// Extract card name and set from OCR text
const extractCardInfo = (text: string) => {
const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
// Common patterns for different card games
const patterns = {
// Magic: The Gathering patterns
mtg: {
cardName: /^[A-Z][a-zA-Z\s,'-]+(?=\s|$)/,
setInfo: /\b([A-Z]{3}|[A-Z]{4})\b/, // 3-4 letter set codes
},
// Pokemon patterns
pokemon: {
cardName: /^[A-Z][a-zA-Z\s]+(?=\s+[0-9])/,
hpPattern: /HP\s*[0-9]+/,
},
// Lorcana patterns
lorcana: {
cardName: /^[A-Z][a-zA-Z\s,'-]+(?=\s*-)/,
subtitle: /-\s*([A-Z][a-zA-Z\s]+)/,
}
};
let cardName = '';
let setName = '';
// Try to find the card name (usually the first substantial line)
for (const line of lines) {
if (line.length > 3 && line.length < 50) { // Reasonable card name length
// Skip common non-name text
if (!line.match(/^(hp|©|legendary|instant|sorcery|creature|artifact|enchantment)$/i)) {
if (!cardName || line.length > cardName.length) {
cardName = line;
}
}
}
}
// Try to extract set information
const setText = lines.find(line => patterns.mtg.setInfo.test(line));
if (setText) {
const setMatch = setText.match(patterns.mtg.setInfo);
if (setMatch) {
setName = setMatch[1];
}
}
return {
name: cardName || '',
set: setName || ''
};
};
// Cleanup on unmount
useEffect(() => {
return () => {
stopCamera();
};
}, []);
return (
<div className="camera-scanner">
{/* Camera Controls */}
<div className="flex gap-4 mb-4">
{!isStreaming ? (
<button
onClick={startCamera}
className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-lg font-medium transition-colors flex items-center gap-2"
>
<span>📹</span> Start Camera
</button>
) : (
<>
<button
onClick={captureImage}
disabled={isProcessing}
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium transition-colors flex items-center gap-2"
>
<span>📸</span>
{isProcessing ? 'Processing...' : 'Capture Card'}
</button>
<button
onClick={stopCamera}
className="bg-red-600 hover:bg-red-700 text-white px-6 py-3 rounded-lg font-medium transition-colors flex items-center gap-2"
>
<span></span> Stop Camera
</button>
</>
)}
</div>
{/* Camera Preview */}
{isStreaming && (
<div className="relative bg-black rounded-lg overflow-hidden mb-4">
<video
ref={videoRef}
autoPlay
playsInline
muted
className="w-full h-auto max-h-96 object-cover"
/>
{/* Scan Guide Overlay */}
<div className="absolute inset-0 pointer-events-none">
<div className="absolute inset-4 border-2 border-white border-dashed rounded-lg flex items-center justify-center">
<div className="bg-black bg-opacity-50 text-white px-4 py-2 rounded-lg text-sm text-center">
<div className="font-medium">Position card within this area</div>
<div className="text-xs opacity-75">Ensure good lighting and focus</div>
</div>
</div>
</div>
</div>
)}
{/* Hidden canvas for image capture */}
<canvas ref={canvasRef} className="hidden" />
{/* Processing Status */}
{isProcessing && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<div className="flex items-center gap-3">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<div>
<div className="font-medium text-blue-900">Processing image...</div>
<div className="text-sm text-blue-600">Extracting card information</div>
</div>
</div>
</div>
)}
{/* Captured Image Preview */}
{capturedImage && !isProcessing && (
<div className="bg-white rounded-lg border border-gray-200 p-4 mb-4">
<div className="font-medium text-gray-900 mb-2">Captured Image</div>
<img
src={capturedImage}
alt="Captured card"
className="max-w-full h-auto max-h-48 rounded border border-gray-300"
/>
</div>
)}
{/* OCR Results */}
{scanResult && (
<div className="bg-white rounded-lg border border-gray-200 p-4">
<div className="font-medium text-gray-900 mb-2">Scan Results</div>
{scanResult.cardName ? (
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-green-600"></span>
<div>
<div className="font-medium">Card Found: {scanResult.cardName}</div>
{scanResult.setName && (
<div className="text-sm text-gray-600">Set: {scanResult.setName}</div>
)}
</div>
</div>
<div className="text-sm text-gray-500">
Confidence: {Math.round(scanResult.confidence)}%
</div>
</div>
) : (
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-yellow-600"></span>
<div className="font-medium">Card not recognized</div>
</div>
<details className="text-sm text-gray-600">
<summary className="cursor-pointer">View OCR text</summary>
<pre className="mt-2 whitespace-pre-wrap bg-gray-50 p-2 rounded text-xs">
{scanResult.text}
</pre>
</details>
</div>
)}
</div>
)}
{/* Instructions */}
<div className="bg-gray-50 rounded-lg p-4 mt-4">
<div className="font-medium text-gray-900 mb-2">📋 Scanning Tips</div>
<ul className="text-sm text-gray-600 space-y-1">
<li> Ensure good lighting and avoid shadows</li>
<li> Keep the card flat and in focus</li>
<li> Position the card name clearly in view</li>
<li> Avoid glare and reflections</li>
<li> Works best with English cards</li>
</ul>
</div>
</div>
);
};
export default CameraScanner;