feat: verification pages

This commit is contained in:
2025-06-07 00:12:41 +02:00
parent a9e382d713
commit eaa92d2fe4
5 changed files with 326 additions and 0 deletions

View File

@ -0,0 +1,180 @@
import { Button } from "@/components/ui/button";
import { useAuth } from "@/store/auth";
import { User } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type FC } from "react";
import { Link } from "react-router";
const VerifyAvatarPage: FC = () => {
const profile = useAuth((s) => s.profile);
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [stream, setStream] = useState<MediaStream | null>(null);
const [avatar, setAvatar] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [takingPicture, setTakingPicture] = useState(false);
const fileInputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (profile?.profile_picture) setAvatar(profile.profile_picture);
}, [profile?.profile_picture]);
// Request camera stream
useEffect(() => {
if (!takingPicture) return;
if (!navigator.mediaDevices?.getUserMedia) {
setError("Camera not supported on this device/browser.");
return;
}
navigator.mediaDevices
.getUserMedia({ video: true })
.then((mediaStream) => {
setStream(mediaStream);
setError(null);
if (videoRef.current) {
videoRef.current.srcObject = mediaStream;
}
})
.catch(() => setError("Unable to access camera."));
return () => {
// Clean up camera stream when component unmounts or stops taking picture
if (stream) {
stream.getTracks().forEach((track) => track.stop());
setStream(null);
}
};
// eslint-disable-next-line
}, [takingPicture]);
const handleTakePicture = useCallback(() => {
setTakingPicture(true);
}, []);
const handleCapture = useCallback(() => {
if (!videoRef.current || !canvasRef.current) return;
const context = canvasRef.current.getContext("2d");
if (!context) return;
// Set canvas size to video size
canvasRef.current.width = videoRef.current.videoWidth;
canvasRef.current.height = videoRef.current.videoHeight;
context.drawImage(
videoRef.current,
0,
0,
videoRef.current.videoWidth,
videoRef.current.videoHeight,
);
const imageData = canvasRef.current.toDataURL("image/png");
setAvatar(imageData);
setTakingPicture(false);
}, []);
const handleSelectFromDevice = useCallback(() => {
fileInputRef.current?.click();
}, []);
const handleFileChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
setError("Please select an image file.");
return;
}
const reader = new FileReader();
reader.onload = (event) => {
setAvatar(event.target?.result as string);
setError(null);
};
reader.readAsDataURL(file);
},
[],
);
const handleRetake = useCallback(() => {
setAvatar(null);
setTakingPicture(false);
}, []);
return (
<div className="w-full sm:max-w-sm mx-auto p-4">
<div className="flex flex-col gap-2 w-full max-w-xs mx-auto">
<h1 className="text-xl font-medium dark:text-gray-200">
Profile Picture
</h1>
<p className="dark:text-gray-400 mb-6">
Please take a photo of yourself for your avatar in order to continue.
</p>
<div className="relative w-48 h-48 mx-auto rounded-full border-4 border-blue-500 overflow-hidden bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-6">
{takingPicture ? (
<video
ref={videoRef}
autoPlay
playsInline
className="w-full h-full object-cover rounded-lg bg-black"
/>
) : avatar ? (
<img
src={avatar}
alt="Avatar"
className="w-full h-full object-cover"
/>
) : (
<span className="text-4xl text-gray-400">
<User size={48} />
</span>
)}
</div>
{error && <div className="text-red-500 text-center">{error}</div>}
{/* File input (hidden) */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleFileChange}
/>
{!avatar && !takingPicture && (
<Button onClick={handleTakePicture}>Take Photo</Button>
)}
{!avatar && !takingPicture && (
<Button variant="outlined" onClick={handleSelectFromDevice}>
Pick from Device
</Button>
)}
{takingPicture && (
<div className="flex flex-col items-center gap-2 w-full">
<Button onClick={handleCapture}>Capture</Button>
<Button variant="outlined" onClick={() => setTakingPicture(false)}>
Cancel
</Button>
</div>
)}
{/* Hidden canvas for snapshot */}
<canvas ref={canvasRef} style={{ display: "none" }} />
{avatar && (
<>
<Link to="/verify/review" className="w-full">
<Button className="w-full">Next</Button>
</Link>
<Button
className="border-yellow-500 text-yellow-500 hover:border-yellow-600 hover:text-yellow-600"
variant="outlined"
onClick={handleRetake}
>
Retake/Choose Another
</Button>
</>
)}
</div>
</div>
);
};
export default VerifyAvatarPage;