← back to Handbag Auth Nextjs
src/app/camera/page.tsx
442 lines
'use client'
import { useState, useRef } from 'react'
import Link from 'next/link'
import Image from 'next/image'
export default function CameraPage() {
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
const [previews, setPreviews] = useState<string[]>([])
const [analyzing, setAnalyzing] = useState(false)
const [result, setResult] = useState<any>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
const [cameraActive, setCameraActive] = useState(false)
const [captureCount, setCaptureCount] = useState(0)
const [showHints, setShowHints] = useState(true)
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || [])
setSelectedFiles(files)
// Create previews
const newPreviews = files.map(file => URL.createObjectURL(file))
setPreviews(newPreviews)
setResult(null)
}
const removePhoto = (index: number) => {
setSelectedFiles(selectedFiles.filter((_, i) => i !== index))
setPreviews(previews.filter((_, i) => i !== index))
}
const startCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }
})
if (videoRef.current) {
videoRef.current.srcObject = stream
setCameraActive(true)
}
} catch (error) {
console.error('Error accessing camera:', error)
alert('Could not access camera. Please use file upload instead.')
}
}
const capturePhoto = () => {
if (!videoRef.current) return
const canvas = document.createElement('canvas')
canvas.width = videoRef.current.videoWidth
canvas.height = videoRef.current.videoHeight
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.drawImage(videoRef.current, 0, 0)
canvas.toBlob((blob) => {
if (!blob) return
const count = captureCount + 1
const file = new File([blob], `handbag-photo-${count}.jpg`, { type: 'image/jpeg' })
// Add to existing files instead of replacing
const newFiles = [...selectedFiles, file]
setSelectedFiles(newFiles)
setPreviews([...previews, URL.createObjectURL(file)])
setCaptureCount(count)
// Auto-stop camera after 4 photos
if (count >= 4) {
stopCamera()
}
}, 'image/jpeg', 0.95)
}
const stopCamera = () => {
if (videoRef.current?.srcObject) {
const stream = videoRef.current.srcObject as MediaStream
stream.getTracks().forEach(track => track.stop())
setCameraActive(false)
}
}
const analyzeImages = async () => {
if (selectedFiles.length === 0) {
alert('Please select or capture at least one image')
return
}
setAnalyzing(true)
setResult(null)
try {
// Convert files to base64
const filePromises = selectedFiles.map(file => {
return new Promise<{data: string, type: string}>((resolve) => {
const reader = new FileReader()
reader.onloadend = () => {
resolve({
data: reader.result as string,
type: file.type
})
}
reader.readAsDataURL(file)
})
})
const files = await Promise.all(filePromises)
const response = await fetch('/api/analyze/image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ files })
})
const data = await response.json()
if (data.success) {
setResult(data.analysis)
} else {
alert('Analysis failed: ' + data.error)
}
} catch (error) {
console.error('Analysis error:', error)
alert('Analysis failed. Please try again.')
} finally {
setAnalyzing(false)
}
}
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-4">
<Link href="/" className="flex items-center space-x-2">
<div className="w-10 h-10 bg-gradient-to-br from-purple-600 to-blue-500 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-xl">HB</span>
</div>
<span className="text-xl font-bold text-gray-900">Handbag Auth</span>
</Link>
<Link
href="/"
className="text-gray-700 hover:text-gray-900 font-medium"
>
Back to Deals
</Link>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="bg-white rounded-lg shadow-lg p-6 md:p-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
AI Handbag Authentication
</h1>
<p className="text-gray-600 mb-8">
Upload or capture photos of your handbag for instant AI-powered authentication and price analysis
</p>
{/* Camera/Upload Section */}
<div className="space-y-4 mb-8">
{!cameraActive ? (
<div className="space-y-4">
<button
onClick={startCamera}
className="w-full bg-gradient-to-r from-purple-600 to-blue-500 text-white py-4 rounded-lg font-medium hover:shadow-lg transition-all flex items-center justify-center space-x-2"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>Use Camera</span>
</button>
<div className="relative">
<input
ref={fileInputRef}
type="file"
accept="image/*,video/*"
multiple
onChange={handleFileChange}
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="w-full bg-white border-2 border-dashed border-gray-300 text-gray-700 py-4 rounded-lg font-medium hover:border-purple-500 hover:bg-purple-50 transition-all flex items-center justify-center space-x-2"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<span>Upload Photos/Videos</span>
</button>
</div>
</div>
) : (
<div className="space-y-4">
{/* Camera Hints */}
{showHints && captureCount === 0 && (
<div className="bg-gradient-to-r from-purple-100 to-blue-100 rounded-lg p-4 mb-4">
<div className="flex items-start space-x-3">
<svg className="w-6 h-6 text-purple-600 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900 mb-2">Capture Tips for Best Results:</p>
<ul className="text-xs text-gray-700 space-y-1">
<li>• Take 3-4 photos from different angles</li>
<li>• Include close-ups of logos, stitching, and hardware</li>
<li>• Ensure good lighting (natural light works best)</li>
<li>• Hold camera steady for clear photos</li>
</ul>
<button
onClick={() => setShowHints(false)}
className="text-xs text-purple-600 mt-2 hover:underline"
>
Got it, dismiss
</button>
</div>
</div>
</div>
)}
{/* Camera Preview */}
<div className="relative">
<video
ref={videoRef}
autoPlay
playsInline
className="w-full rounded-lg shadow-xl"
/>
{/* Capture Counter */}
{captureCount > 0 && (
<div className="absolute top-4 right-4 bg-purple-600 text-white px-3 py-1 rounded-full text-sm font-bold shadow-lg">
{captureCount}/4 photos
</div>
)}
{/* Grid Overlay for Composition */}
<div className="absolute inset-0 pointer-events-none">
<div className="w-full h-full grid grid-cols-3 grid-rows-3">
{[...Array(9)].map((_, i) => (
<div key={i} className="border border-white/20" />
))}
</div>
</div>
</div>
{/* Capture Buttons */}
<div className="flex space-x-3">
<button
onClick={capturePhoto}
className="flex-1 bg-gradient-to-r from-purple-600 to-blue-500 text-white py-4 rounded-lg font-medium hover:shadow-xl transition-all flex items-center justify-center space-x-2"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>Capture Photo</span>
</button>
<button
onClick={stopCamera}
className="px-6 bg-gray-200 text-gray-700 py-4 rounded-lg font-medium hover:bg-gray-300 transition-all"
>
Done
</button>
</div>
</div>
)}
</div>
{/* Previews */}
{previews.length > 0 && (
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Selected Images ({previews.length})</h3>
<button
onClick={() => {
setSelectedFiles([])
setPreviews([])
setCaptureCount(0)
}}
className="text-sm text-red-600 hover:text-red-700 font-medium"
>
Clear All
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{previews.map((preview, index) => (
<div key={index} className="relative aspect-square group">
<Image
src={preview}
alt={`Preview ${index + 1}`}
fill
className="object-cover rounded-lg"
/>
<button
onClick={() => removePhoto(index)}
className="absolute top-2 right-2 bg-red-600 text-white p-1.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-lg hover:bg-red-700"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="absolute bottom-2 left-2 bg-black/60 text-white text-xs px-2 py-1 rounded">
Photo {index + 1}
</div>
</div>
))}
</div>
<button
onClick={analyzeImages}
disabled={analyzing}
className="mt-4 w-full bg-gradient-to-r from-green-600 to-teal-500 text-white py-4 rounded-lg font-medium hover:shadow-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
>
{analyzing ? (
<>
<svg className="animate-spin h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Analyzing with AI...</span>
</>
) : (
<>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Analyze Handbag</span>
</>
)}
</button>
</div>
)}
{/* Results */}
{result && (
<div className="border-t pt-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Analysis Results</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Basic Info */}
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-gray-500">Brand</label>
<p className="text-lg font-semibold">{result.brand || 'N/A'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">Model</label>
<p className="text-lg font-semibold">{result.model || 'N/A'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">Material</label>
<p className="text-lg">{result.material || 'N/A'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">Color</label>
<p className="text-lg">{result.color || 'N/A'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">Hardware</label>
<p className="text-lg">{result.hardware || 'N/A'}</p>
</div>
</div>
{/* Condition & Value */}
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-gray-500">Condition</label>
<p className="text-lg font-semibold">{result.condition || 'N/A'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">Estimated Value</label>
<p className="text-2xl font-bold text-green-600">
${result.estimated_value_low?.toLocaleString() || '0'} - ${result.estimated_value_high?.toLocaleString() || '0'}
</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">Authenticity Confidence</label>
<p className={`text-lg font-semibold ${
result.authenticity_confidence === 'High' ? 'text-green-600' :
result.authenticity_confidence === 'Medium' ? 'text-yellow-600' :
'text-red-600'
}`}>
{result.authenticity_confidence || 'N/A'}
</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">Investment Rating</label>
<p className="text-lg font-semibold">{result.investment_rating || 'N/A'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-500">Market Demand</label>
<p className="text-lg font-semibold">{result.demand_level || 'N/A'}</p>
</div>
</div>
</div>
{/* Detailed Analysis */}
<div className="mt-6 space-y-4">
{result.authenticity_markers && (
<div>
<label className="text-sm font-medium text-gray-500">Authenticity Markers</label>
<p className="text-gray-700 mt-1">{result.authenticity_markers}</p>
</div>
)}
{result.red_flags && (
<div>
<label className="text-sm font-medium text-red-600">Red Flags</label>
<p className="text-gray-700 mt-1">{result.red_flags}</p>
</div>
)}
{result.issues && (
<div>
<label className="text-sm font-medium text-gray-500">Visible Issues</label>
<p className="text-gray-700 mt-1">{result.issues}</p>
</div>
)}
{result.best_platforms && (
<div>
<label className="text-sm font-medium text-gray-500">Best Selling Platforms</label>
<p className="text-gray-700 mt-1">{result.best_platforms}</p>
</div>
)}
{result.market_notes && (
<div>
<label className="text-sm font-medium text-gray-500">Market Notes</label>
<p className="text-gray-700 mt-1">{result.market_notes}</p>
</div>
)}
</div>
</div>
)}
</div>
</main>
</div>
)
}