SkateSpots/src/components/CameraCapture.tsx
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

313 lines
9.5 KiB
TypeScript

import { useState, useRef, useCallback, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { useToast } from '@/hooks/useToast';
import { cn } from '@/lib/utils';
import {
Camera,
X,
RotateCcw,
Check,
Loader2,
SwitchCamera,
} from 'lucide-react';
interface CameraCaptureProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onCapture: (file: File) => void;
isUploading?: boolean;
}
export function CameraCapture({
open,
onOpenChange,
onCapture,
isUploading,
}: CameraCaptureProps) {
const { toast } = useToast();
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const streamRef = useRef<MediaStream | null>(null);
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [capturedImage, setCapturedImage] = useState<string | null>(null);
const [facingMode, setFacingMode] = useState<'user' | 'environment'>('environment');
const [isStarting, setIsStarting] = useState(false);
// Start the camera
const startCamera = useCallback(async (facing: 'user' | 'environment' = facingMode) => {
setIsStarting(true);
try {
// Stop any existing stream first
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
}
const constraints: MediaStreamConstraints = {
video: {
facingMode: facing,
width: { ideal: 1920 },
height: { ideal: 1080 },
},
audio: false,
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
streamRef.current = stream;
if (videoRef.current) {
videoRef.current.srcObject = stream;
await videoRef.current.play();
}
setHasPermission(true);
setFacingMode(facing);
} catch (error) {
console.error('Camera access error:', error);
setHasPermission(false);
toast({
title: 'Camera access denied',
description: 'Please allow camera access in your browser settings to take photos.',
variant: 'destructive',
});
} finally {
setIsStarting(false);
}
}, [facingMode, toast]);
// Stop the camera
const stopCamera = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
if (videoRef.current) {
videoRef.current.srcObject = null;
}
}, []);
// Start camera when dialog opens
useEffect(() => {
if (open) {
startCamera();
} else {
stopCamera();
setCapturedImage(null);
setHasPermission(null);
}
return () => {
stopCamera();
};
}, [open, startCamera, stopCamera]);
// Switch camera (front/back)
const switchCamera = () => {
const newFacing = facingMode === 'user' ? 'environment' : 'user';
setCapturedImage(null);
startCamera(newFacing);
};
// Capture photo
const capturePhoto = () => {
if (!videoRef.current || !canvasRef.current) return;
const video = videoRef.current;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Set canvas dimensions to video dimensions
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// Draw the video frame to the canvas
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// Get the data URL
const dataUrl = canvas.toDataURL('image/jpeg', 0.9);
setCapturedImage(dataUrl);
};
// Retake photo
const retakePhoto = () => {
setCapturedImage(null);
};
// Confirm and upload photo
const confirmPhoto = async () => {
if (!capturedImage || !canvasRef.current) return;
// Convert data URL to Blob then to File
const response = await fetch(capturedImage);
const blob = await response.blob();
const file = new File([blob], `skate-spot-${Date.now()}.jpg`, { type: 'image/jpeg' });
onCapture(file);
};
// Handle close
const handleClose = () => {
stopCamera();
setCapturedImage(null);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px] p-0 overflow-hidden bg-black border-[#ff0040]/50">
<DialogHeader className="p-4 border-b border-gray-800">
<DialogTitle className="flex items-center gap-2 text-white">
<Camera className="h-5 w-5 text-[#ff0040]" />
Take a Photo
</DialogTitle>
</DialogHeader>
<div className="relative aspect-[4/3] bg-black">
{/* Hidden canvas for capturing */}
<canvas ref={canvasRef} className="hidden" />
{/* Loading state */}
{isStarting && (
<div className="absolute inset-0 flex items-center justify-center bg-black">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin text-[#ccff00] mx-auto mb-2" />
<p className="text-gray-400 text-sm">Starting camera...</p>
</div>
</div>
)}
{/* No permission state */}
{hasPermission === false && (
<div className="absolute inset-0 flex items-center justify-center bg-black">
<div className="text-center p-6">
<Camera className="h-12 w-12 text-gray-600 mx-auto mb-4" />
<h3 className="text-white font-bold mb-2">Camera Access Required</h3>
<p className="text-gray-400 text-sm mb-4">
Please allow camera access in your browser to take photos.
</p>
<Button
onClick={() => startCamera()}
className="bg-[#ff0040] hover:bg-[#ff0040]/90"
>
Try Again
</Button>
</div>
</div>
)}
{/* Video feed */}
{!capturedImage && (
<video
ref={videoRef}
autoPlay
playsInline
muted
className={cn(
'w-full h-full object-cover',
(isStarting || hasPermission === false) && 'hidden'
)}
/>
)}
{/* Captured image preview */}
{capturedImage && (
<img
src={capturedImage}
alt="Captured"
className="w-full h-full object-cover"
/>
)}
{/* Camera switch button */}
{!capturedImage && hasPermission && !isStarting && (
<Button
variant="ghost"
size="icon"
onClick={switchCamera}
className="absolute top-2 right-2 bg-black/50 hover:bg-black/70 text-white"
>
<SwitchCamera className="h-5 w-5" />
</Button>
)}
{/* Crosshairs/guides */}
{!capturedImage && hasPermission && !isStarting && (
<div className="absolute inset-0 pointer-events-none">
{/* Grid lines */}
<div className="absolute inset-0 grid grid-cols-3 grid-rows-3">
<div className="border-r border-white/20" />
<div className="border-r border-white/20" />
<div />
<div className="border-r border-t border-white/20" />
<div className="border-r border-t border-white/20" />
<div className="border-t border-white/20" />
<div className="border-r border-t border-white/20" />
<div className="border-r border-t border-white/20" />
<div className="border-t border-white/20" />
</div>
</div>
)}
</div>
{/* Controls */}
<div className="p-4 bg-black border-t border-gray-800">
{!capturedImage ? (
<div className="flex items-center justify-center gap-4">
<Button
variant="outline"
onClick={handleClose}
className="border-gray-700 text-gray-400 hover:bg-gray-900"
>
Cancel
</Button>
<Button
onClick={capturePhoto}
disabled={!hasPermission || isStarting}
className="bg-[#ff0040] hover:bg-[#ff0040]/90 text-white px-8 h-12 rounded-full"
>
<Camera className="h-5 w-5" />
</Button>
</div>
) : (
<div className="flex items-center justify-center gap-4">
<Button
variant="outline"
onClick={retakePhoto}
disabled={isUploading}
className="gap-2 border-gray-700 text-gray-400 hover:bg-gray-900"
>
<RotateCcw className="h-4 w-4" />
Retake
</Button>
<Button
onClick={confirmPhoto}
disabled={isUploading}
className="gap-2 bg-[#ccff00] hover:bg-[#ccff00]/90 text-black font-bold"
>
{isUploading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Uploading...
</>
) : (
<>
<Check className="h-4 w-4" />
Use Photo
</>
)}
</Button>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}