298 lines
12 KiB
TypeScript
298 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { X } from 'lucide-react';
|
||
|
||
interface CalculatorProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onUseResult?: (value: number) => void;
|
||
}
|
||
|
||
export default function Calculator({ isOpen, onClose, onUseResult }: CalculatorProps) {
|
||
const [display, setDisplay] = useState('0');
|
||
const [operator, setOperator] = useState('');
|
||
const [firstValue, setFirstValue] = useState('');
|
||
const [waitingForSecond, setWaitingForSecond] = useState(false);
|
||
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||
const [lastExpression, setLastExpression] = useState<string>("");
|
||
|
||
// Initialiser la position au centre à l'ouverture
|
||
useEffect(() => {
|
||
if (isOpen && position === null) {
|
||
const centerX = (window.innerWidth - 280) / 2;
|
||
const centerY = (window.innerHeight - 450) / 2;
|
||
setPosition({ x: centerX, y: centerY });
|
||
}
|
||
}, [isOpen, position]);
|
||
|
||
// Réinitialiser la position quand on ferme
|
||
useEffect(() => {
|
||
if (!isOpen) {
|
||
setPosition(null);
|
||
}
|
||
}, [isOpen]);
|
||
|
||
const resetCalculator = () => {
|
||
setDisplay('0');
|
||
setOperator('');
|
||
setFirstValue('');
|
||
setWaitingForSecond(false);
|
||
setLastExpression("");
|
||
};
|
||
|
||
const handleNumber = (num: string) => {
|
||
if (waitingForSecond) {
|
||
setDisplay(num);
|
||
setWaitingForSecond(false);
|
||
// Si on était après un calcul (operator vide), effacer l'expression précédente
|
||
if (!operator) setLastExpression("");
|
||
} else {
|
||
setDisplay(display === '0' ? num : display + num);
|
||
}
|
||
};
|
||
|
||
const handleOperator = (op: string) => {
|
||
if (operator && !waitingForSecond) {
|
||
calculate();
|
||
}
|
||
setFirstValue(display);
|
||
setOperator(op);
|
||
setWaitingForSecond(true);
|
||
};
|
||
|
||
const calculate = () => {
|
||
const first = parseFloat(firstValue);
|
||
const second = parseFloat(display);
|
||
let result = 0;
|
||
|
||
switch(operator) {
|
||
case '+': result = first + second; break;
|
||
case '-': result = first - second; break;
|
||
case '*': result = first * second; break;
|
||
case '/': result = second !== 0 ? first / second : 0; break;
|
||
}
|
||
|
||
// Arrondir et afficher 2 décimales dans la calculatrice
|
||
const rounded = Math.round((result + Number.EPSILON) * 100) / 100;
|
||
// Conserver l'expression utilisée
|
||
const opSymbol = operator === '/' ? '÷' : operator === '*' ? '×' : operator === '-' ? '−' : operator;
|
||
const fmt = (s: string) => s.replace('.', ',');
|
||
setLastExpression(`${fmt(firstValue)} ${opSymbol} ${fmt(display)} =`);
|
||
setDisplay(rounded.toFixed(2));
|
||
setOperator('');
|
||
setFirstValue('');
|
||
// Après un calcul, la prochaine saisie numérique doit remplacer l'affichage
|
||
setWaitingForSecond(true);
|
||
};
|
||
|
||
const handleDecimal = () => {
|
||
if (waitingForSecond) {
|
||
setDisplay('0.');
|
||
setWaitingForSecond(false);
|
||
} else if (!display.includes('.')) {
|
||
setDisplay(display + '.');
|
||
}
|
||
};
|
||
|
||
const handleMouseDown = (e: React.MouseEvent) => {
|
||
if (!position) return;
|
||
setIsDragging(true);
|
||
setDragOffset({
|
||
x: e.clientX - position.x,
|
||
y: e.clientY - position.y,
|
||
});
|
||
};
|
||
|
||
useEffect(() => {
|
||
const handleMouseMove = (e: MouseEvent) => {
|
||
if (!isDragging) return;
|
||
|
||
const newX = e.clientX - dragOffset.x;
|
||
const newY = e.clientY - dragOffset.y;
|
||
|
||
const maxX = window.innerWidth - 280;
|
||
const maxY = window.innerHeight - 450;
|
||
|
||
setPosition({
|
||
x: Math.max(0, Math.min(newX, maxX)),
|
||
y: Math.max(0, Math.min(newY, maxY)),
|
||
});
|
||
};
|
||
|
||
const handleMouseUp = () => {
|
||
setIsDragging(false);
|
||
};
|
||
|
||
if (isDragging) {
|
||
document.addEventListener('mousemove', handleMouseMove);
|
||
document.addEventListener('mouseup', handleMouseUp);
|
||
}
|
||
|
||
return () => {
|
||
document.removeEventListener('mousemove', handleMouseMove);
|
||
document.removeEventListener('mouseup', handleMouseUp);
|
||
};
|
||
}, [isDragging, dragOffset]);
|
||
|
||
const handleUse = () => {
|
||
const result = parseFloat(display);
|
||
if (!isNaN(result) && result > 0 && onUseResult) {
|
||
const rounded = Math.round((result + Number.EPSILON) * 100) / 100;
|
||
onUseResult(rounded);
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
// Gestion du clavier
|
||
useEffect(() => {
|
||
if (!isOpen) return;
|
||
|
||
const handleKeyDown = (e: KeyboardEvent) => {
|
||
// Vérifier que le focus est sur la calculatrice ou ses enfants
|
||
if (calculatorRef.current && !calculatorRef.current.contains(document.activeElement)) {
|
||
return; // Ignorer si le focus n'est pas sur la calculatrice
|
||
}
|
||
|
||
// Empêcher le comportement par défaut pour les touches de la calculatrice
|
||
if (['0','1','2','3','4','5','6','7','8','9','+','-','*','/','Enter','Escape','Backspace','Delete','c','C','.'].includes(e.key)) {
|
||
e.preventDefault();
|
||
}
|
||
|
||
// Chiffres
|
||
if (e.key >= '0' && e.key <= '9') {
|
||
handleNumber(e.key);
|
||
}
|
||
// Opérateurs
|
||
else if (e.key === '+') {
|
||
handleOperator('+');
|
||
}
|
||
else if (e.key === '-') {
|
||
handleOperator('-');
|
||
}
|
||
else if (e.key === '*' || e.key === 'x' || e.key === 'X') {
|
||
handleOperator('*');
|
||
}
|
||
else if (e.key === '/') {
|
||
handleOperator('/');
|
||
}
|
||
// Égal
|
||
else if (e.key === 'Enter' || e.key === '=') {
|
||
if (operator) calculate();
|
||
}
|
||
// Clear
|
||
else if (e.key === 'Escape' || e.key === 'c' || e.key === 'C') {
|
||
resetCalculator();
|
||
}
|
||
// Backspace / Delete
|
||
else if (e.key === 'Backspace' || e.key === 'Delete') {
|
||
if (display.length > 1) {
|
||
setDisplay(display.slice(0, -1));
|
||
} else {
|
||
setDisplay('0');
|
||
}
|
||
}
|
||
// Point décimal
|
||
else if (e.key === '.' || e.key === ',') {
|
||
handleDecimal();
|
||
}
|
||
};
|
||
|
||
window.addEventListener('keydown', handleKeyDown);
|
||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||
}, [isOpen, display, operator, firstValue, waitingForSecond]);
|
||
|
||
// Focus automatique sur la calculatrice à l'ouverture
|
||
const calculatorRef = React.useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
// Focus une fois que le composant est monté (position non nulle)
|
||
if (isOpen && position !== null && calculatorRef.current) {
|
||
calculatorRef.current.focus();
|
||
}
|
||
}, [isOpen, position]);
|
||
|
||
if (!isOpen || position === null) return null;
|
||
|
||
return (
|
||
<div
|
||
ref={calculatorRef}
|
||
tabIndex={-1}
|
||
className="fixed bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden focus:outline-none"
|
||
style={{
|
||
left: `${position.x}px`,
|
||
top: `${position.y}px`,
|
||
width: '280px',
|
||
zIndex: 9999,
|
||
}}
|
||
>
|
||
{/* Header draggable */}
|
||
<div
|
||
className="flex items-center justify-between px-4 py-3 bg-gradient-to-r from-indigo-600 to-indigo-500 cursor-move select-none"
|
||
onMouseDown={handleMouseDown}
|
||
>
|
||
<span className="text-white font-semibold text-sm">Calculatrice</span>
|
||
<button
|
||
onClick={onClose}
|
||
className="w-7 h-7 flex items-center justify-center rounded-lg bg-white/20 hover:bg-white/30 transition-colors"
|
||
onMouseDown={(e) => e.stopPropagation()}
|
||
>
|
||
<X className="w-4 h-4 text-white" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Body */}
|
||
<div className="p-4">
|
||
{/* Display */}
|
||
<div className="relative bg-gradient-to-br from-slate-100 to-slate-200 border-2 border-slate-300 rounded-xl p-4 text-right text-2xl font-semibold text-slate-900 mb-4 min-h-[58px] break-all">
|
||
{/* Opérateur courant (badge) */}
|
||
{operator && (
|
||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm font-semibold text-slate-600 select-none">
|
||
{operator === '/' ? '÷' : operator === '*' ? '×' : operator === '-' ? '−' : operator}
|
||
</span>
|
||
)}
|
||
{/* Dernière expression calculée */}
|
||
{lastExpression && (
|
||
<div className="absolute left-3 top-1.5 text-xs font-medium text-slate-500 select-none">
|
||
{lastExpression}
|
||
</div>
|
||
)}
|
||
{display || '0'}
|
||
</div>
|
||
|
||
{/* Buttons */}
|
||
<div className="grid grid-cols-4 gap-2">
|
||
<button onClick={() => resetCalculator()} className="col-span-1 p-4 rounded-lg font-semibold bg-gradient-to-br from-red-500 to-red-600 text-white hover:from-red-600 hover:to-red-700 transition-all hover:shadow-md active:scale-95">C</button>
|
||
<button onClick={() => handleOperator('/')} className="p-4 rounded-lg font-semibold bg-gradient-to-br from-orange-400 to-orange-500 text-white hover:from-orange-500 hover:to-orange-600 transition-all hover:shadow-md active:scale-95">/</button>
|
||
<button onClick={() => handleOperator('*')} className="p-4 rounded-lg font-semibold bg-gradient-to-br from-orange-400 to-orange-500 text-white hover:from-orange-500 hover:to-orange-600 transition-all hover:shadow-md active:scale-95">×</button>
|
||
<button onClick={() => handleOperator('-')} className="p-4 rounded-lg font-semibold bg-gradient-to-br from-orange-400 to-orange-500 text-white hover:from-orange-500 hover:to-orange-600 transition-all hover:shadow-md active:scale-95">−</button>
|
||
|
||
<button onClick={() => handleNumber('7')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">7</button>
|
||
<button onClick={() => handleNumber('8')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">8</button>
|
||
<button onClick={() => handleNumber('9')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">9</button>
|
||
<button onClick={() => handleOperator('+')} className="row-span-2 p-4 rounded-lg font-semibold bg-gradient-to-br from-orange-400 to-orange-500 text-white hover:from-orange-500 hover:to-orange-600 transition-all hover:shadow-md active:scale-95">+</button>
|
||
|
||
<button onClick={() => handleNumber('4')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">4</button>
|
||
<button onClick={() => handleNumber('5')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">5</button>
|
||
<button onClick={() => handleNumber('6')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">6</button>
|
||
|
||
<button onClick={() => handleNumber('1')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">1</button>
|
||
<button onClick={() => handleNumber('2')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">2</button>
|
||
<button onClick={() => handleNumber('3')} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">3</button>
|
||
<button onClick={() => operator ? calculate() : null} className="row-span-2 p-4 rounded-lg font-semibold bg-gradient-to-br from-green-500 to-green-600 text-white hover:from-green-600 hover:to-green-700 transition-all hover:shadow-md active:scale-95">=</button>
|
||
|
||
<button onClick={() => handleNumber('0')} className="col-span-2 p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">0</button>
|
||
<button onClick={handleDecimal} className="p-4 rounded-lg font-semibold bg-white border border-slate-200 hover:bg-slate-50 transition-all hover:shadow-md active:scale-95">.</button>
|
||
|
||
{onUseResult && (
|
||
<button onClick={handleUse} className="col-span-4 mt-2 p-3 rounded-lg font-semibold text-sm bg-gradient-to-br from-indigo-600 to-indigo-500 text-white hover:from-indigo-700 hover:to-indigo-600 transition-all hover:shadow-md active:scale-95">
|
||
Utiliser le résultat
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|