branch backend + ajout du serveur backend, next, et sqlite
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface Point {
|
||||
norm_x: number;
|
||||
norm_y: number;
|
||||
}
|
||||
|
||||
interface Impact {
|
||||
id: number;
|
||||
label: string;
|
||||
score: number;
|
||||
coords: Point;
|
||||
}
|
||||
|
||||
interface PhotoOverlayProps {
|
||||
imageUrl: string;
|
||||
impacts: Impact[];
|
||||
targetCorners: Point[];
|
||||
isEditing?: boolean;
|
||||
onImpactsChange?: (impacts: Impact[]) => void;
|
||||
}
|
||||
|
||||
export default function PhotoOverlay({
|
||||
imageUrl,
|
||||
impacts,
|
||||
targetCorners,
|
||||
isEditing = false,
|
||||
onImpactsChange
|
||||
}: PhotoOverlayProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (containerRef.current) {
|
||||
setDimensions({
|
||||
width: containerRef.current.offsetWidth,
|
||||
height: containerRef.current.offsetHeight,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
return () => window.removeEventListener("resize", updateDimensions);
|
||||
}, []);
|
||||
|
||||
const handleImageClick = (e: React.MouseEvent) => {
|
||||
if (!isEditing || !onImpactsChange || dragIndex !== null) return;
|
||||
|
||||
const rect = containerRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
// Calculer les coordonnées normalisées
|
||||
const x = (e.clientX - rect.left) / dimensions.width;
|
||||
const y = (e.clientY - rect.top) / dimensions.height;
|
||||
|
||||
// Créer un nouvel impact
|
||||
const newImpact: Impact = {
|
||||
id: impacts.length > 0 ? Math.max(...impacts.map(i => i.id)) + 1 : 1,
|
||||
label: "bullet_hole",
|
||||
score: 10,
|
||||
coords: { norm_x: x, norm_y: y }
|
||||
};
|
||||
|
||||
onImpactsChange([...impacts, newImpact]);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (dragIndex === null || !onImpactsChange) return;
|
||||
|
||||
const rect = containerRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
|
||||
const x = (e.clientX - rect.left) / dimensions.width;
|
||||
const y = (e.clientY - rect.top) / dimensions.height;
|
||||
|
||||
const newImpacts = [...impacts];
|
||||
newImpacts[dragIndex] = {
|
||||
...newImpacts[dragIndex],
|
||||
coords: { norm_x: x, norm_y: y }
|
||||
};
|
||||
|
||||
onImpactsChange(newImpacts);
|
||||
};
|
||||
|
||||
const deleteImpact = (e: React.MouseEvent, index: number) => {
|
||||
e.stopPropagation();
|
||||
if (!isEditing || !onImpactsChange) return;
|
||||
const newImpacts = impacts.filter((_, i) => i !== index);
|
||||
onImpactsChange(newImpacts);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`relative w-full flex items-center justify-center bg-slate-900 rounded-2xl overflow-hidden border border-slate-800 shadow-2xl min-h-[400px] ${isEditing ? 'cursor-crosshair' : ''}`}
|
||||
onClick={handleImageClick}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={() => setDragIndex(null)}
|
||||
onMouseLeave={() => setDragIndex(null)}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Target Analysis"
|
||||
className="max-w-full max-h-[80vh] object-contain select-none"
|
||||
draggable={false}
|
||||
onLoad={(e) => {
|
||||
const img = e.currentTarget;
|
||||
setDimensions({ width: img.clientWidth, height: img.clientHeight });
|
||||
}}
|
||||
/>
|
||||
|
||||
{dimensions.width > 0 && (
|
||||
<svg
|
||||
className="absolute pointer-events-none"
|
||||
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
|
||||
style={{ width: dimensions.width, height: dimensions.height }}
|
||||
>
|
||||
{/* Draw Target Box if corners exist */}
|
||||
{targetCorners && targetCorners.length === 4 && (
|
||||
<polygon
|
||||
points={targetCorners.map(c => `${c.norm_x * dimensions.width},${c.norm_y * dimensions.height}`).join(' ')}
|
||||
fill="rgba(79, 70, 229, 0.15)"
|
||||
stroke="#6366f1"
|
||||
strokeWidth="2"
|
||||
strokeDasharray="4 2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Draw Impacts */}
|
||||
{impacts.map((impact, index) => (
|
||||
<g key={impact.id}>
|
||||
{/* Glow effect */}
|
||||
<circle
|
||||
cx={impact.coords.norm_x * dimensions.width}
|
||||
cy={impact.coords.norm_y * dimensions.height}
|
||||
r={isEditing ? "12" : "8"}
|
||||
fill={isEditing ? "rgba(244, 63, 94, 0.2)" : "rgba(244, 63, 94, 0.3)"}
|
||||
className={isEditing ? 'pointer-events-auto cursor-move' : ''}
|
||||
onMouseDown={(e) => {
|
||||
if (!isEditing) return;
|
||||
e.stopPropagation();
|
||||
setDragIndex(index);
|
||||
}}
|
||||
onContextMenu={(e) => deleteImpact(e, index)}
|
||||
/>
|
||||
{/* Main circle */}
|
||||
<circle
|
||||
cx={impact.coords.norm_x * dimensions.width}
|
||||
cy={impact.coords.norm_y * dimensions.height}
|
||||
r="5"
|
||||
fill="#f43f5e"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
{/* Score text */}
|
||||
<text
|
||||
x={impact.coords.norm_x * dimensions.width}
|
||||
y={impact.coords.norm_y * dimensions.height - 10}
|
||||
className="text-[12px] font-bold fill-white text-center select-none"
|
||||
textAnchor="middle"
|
||||
style={{ textShadow: '0 0 4px rgba(0,0,0,0.9), 1px 1px 0 rgba(0,0,0,0.9)' }}
|
||||
>
|
||||
{impact.score}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{isEditing && (
|
||||
<div className="absolute bottom-4 right-4 bg-slate-950/80 backdrop-blur-md px-3 py-1.5 rounded-full text-[10px] font-bold text-indigo-400 border border-indigo-500/30">
|
||||
MODE ÉDITION : CLIC POUR AJOUTER • DRAG POUR DÉPLACER • CLIC DROIT POUR SUPPRIMER
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user