Files
openmonetis/components/calculadora/use-calculator-state.ts
2026-03-06 16:39:49 +00:00

380 lines
8.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { VariantProps } from "class-variance-authority";
import { useEffect, useRef, useState } from "react";
import type { buttonVariants } from "@/components/ui/button";
import {
formatLocaleValue,
formatNumber,
normalizeClipboardNumber,
OPERATOR_SYMBOLS,
type Operator,
performOperation,
} from "@/lib/utils/calculator";
export type CalculatorButtonConfig = {
label: string;
onClick: () => void;
variant?: VariantProps<typeof buttonVariants>["variant"];
colSpan?: number;
className?: string;
};
export function useCalculatorState() {
const [display, setDisplay] = useState("0");
const [accumulator, setAccumulator] = useState<number | null>(null);
const [operator, setOperator] = useState<Operator | null>(null);
const [overwrite, setOverwrite] = useState(false);
const [history, setHistory] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const resetCopiedTimeoutRef = useRef<number | undefined>(undefined);
const currentValue = Number(display);
const resultText = (() => {
if (display === "Erro") {
return null;
}
const normalized = formatNumber(currentValue);
if (normalized === "Erro") {
return null;
}
return formatLocaleValue(normalized);
})();
const reset = () => {
setDisplay("0");
setAccumulator(null);
setOperator(null);
setOverwrite(false);
setHistory(null);
};
const inputDigit = (digit: string) => {
// Check conditions before state updates
const shouldReset = overwrite || display === "Erro";
setDisplay((prev) => {
if (shouldReset) {
return digit;
}
if (prev === "0") {
return digit;
}
// Limitar a 10 dígitos (excluindo sinal negativo e ponto decimal)
const digitCount = prev.replace(/[-.]/g, "").length;
if (digitCount >= 10) {
return prev;
}
return `${prev}${digit}`;
});
// Update related states after display update
if (shouldReset) {
setOverwrite(false);
setHistory(null);
}
};
const inputDecimal = () => {
// Check conditions before state updates
const shouldReset = overwrite || display === "Erro";
setDisplay((prev) => {
if (shouldReset) {
return "0.";
}
if (prev.includes(".")) {
return prev;
}
// Limitar a 10 dígitos antes de adicionar o ponto decimal
const digitCount = prev.replace(/[-]/g, "").length;
if (digitCount >= 10) {
return prev;
}
return `${prev}.`;
});
// Update related states after display update
if (shouldReset) {
setOverwrite(false);
setHistory(null);
}
};
const setNextOperator = (nextOperator: Operator) => {
if (display === "Erro") {
reset();
return;
}
const value = currentValue;
if (accumulator === null || operator === null || overwrite) {
setAccumulator(value);
} else {
const result = performOperation(accumulator, value, operator);
const formatted = formatNumber(result);
setAccumulator(Number.isFinite(result) ? result : null);
setDisplay(formatted);
if (!Number.isFinite(result)) {
setOperator(null);
setOverwrite(true);
setHistory(null);
return;
}
}
setOperator(nextOperator);
setOverwrite(true);
setHistory(null);
};
const evaluate = () => {
if (operator === null || accumulator === null || display === "Erro") {
return;
}
const value = currentValue;
const left = formatNumber(accumulator);
const right = formatNumber(value);
const symbol = OPERATOR_SYMBOLS[operator];
const operation = `${formatLocaleValue(left)} ${symbol} ${formatLocaleValue(
right,
)}`;
const result = performOperation(accumulator, value, operator);
const formatted = formatNumber(result);
setDisplay(formatted);
setAccumulator(Number.isFinite(result) ? result : null);
setOperator(null);
setOverwrite(true);
setHistory(operation);
};
const toggleSign = () => {
setDisplay((prev) => {
if (prev === "Erro") {
return prev;
}
if (prev.startsWith("-")) {
return prev.slice(1);
}
return prev === "0" ? prev : `-${prev}`;
});
if (overwrite) {
setOverwrite(false);
setHistory(null);
}
};
const deleteLastDigit = () => {
setHistory(null);
// Check conditions before state updates
const isError = display === "Erro";
setDisplay((prev) => {
if (prev === "Erro") {
return "0";
}
if (overwrite) {
return "0";
}
if (prev.length <= 1 || (prev.length === 2 && prev.startsWith("-"))) {
return "0";
}
return prev.slice(0, -1);
});
// Update related states after display update
if (isError) {
setAccumulator(null);
setOperator(null);
setOverwrite(false);
} else if (overwrite) {
setOverwrite(false);
}
};
const applyPercent = () => {
setDisplay((prev) => {
if (prev === "Erro") {
return prev;
}
const value = Number(prev);
return formatNumber(value / 100);
});
setOverwrite(true);
setHistory(null);
};
const expression = (() => {
if (display === "Erro") {
return "Erro";
}
if (operator && accumulator !== null) {
const symbol = OPERATOR_SYMBOLS[operator];
const left = formatLocaleValue(formatNumber(accumulator));
if (overwrite) {
return `${left} ${symbol}`;
}
return `${left} ${symbol} ${formatLocaleValue(display)}`;
}
return formatLocaleValue(display);
})();
const makeOperatorHandler = (nextOperator: Operator) => () =>
setNextOperator(nextOperator);
const buttons: CalculatorButtonConfig[][] = [
[
{ label: "C", onClick: reset, variant: "destructive" },
{ label: "⌫", onClick: deleteLastDigit, variant: "secondary" },
{ label: "%", onClick: applyPercent, variant: "secondary" },
{
label: "÷",
onClick: makeOperatorHandler("divide"),
variant: "outline",
},
],
[
{ label: "7", onClick: () => inputDigit("7") },
{ label: "8", onClick: () => inputDigit("8") },
{ label: "9", onClick: () => inputDigit("9") },
{
label: "×",
onClick: makeOperatorHandler("multiply"),
variant: "outline",
},
],
[
{ label: "4", onClick: () => inputDigit("4") },
{ label: "5", onClick: () => inputDigit("5") },
{ label: "6", onClick: () => inputDigit("6") },
{
label: "-",
onClick: makeOperatorHandler("subtract"),
variant: "outline",
},
],
[
{ label: "1", onClick: () => inputDigit("1") },
{ label: "2", onClick: () => inputDigit("2") },
{ label: "3", onClick: () => inputDigit("3") },
{ label: "+", onClick: makeOperatorHandler("add"), variant: "outline" },
],
[
{ label: "±", onClick: toggleSign, variant: "secondary" },
{ label: "0", onClick: () => inputDigit("0") },
{ label: ",", onClick: inputDecimal },
{ label: "=", onClick: evaluate, variant: "default" },
],
];
const copyToClipboard = async () => {
if (!resultText) return;
try {
await navigator.clipboard.writeText(resultText);
setCopied(true);
if (resetCopiedTimeoutRef.current !== undefined) {
window.clearTimeout(resetCopiedTimeoutRef.current);
}
resetCopiedTimeoutRef.current = window.setTimeout(() => {
setCopied(false);
}, 2000);
} catch (error) {
console.error(
"Não foi possível copiar o resultado da calculadora.",
error,
);
}
};
const pasteFromClipboard = async () => {
if (!navigator.clipboard?.readText) return;
try {
const rawValue = await navigator.clipboard.readText();
const normalized = normalizeClipboardNumber(rawValue);
if (resetCopiedTimeoutRef.current !== undefined) {
window.clearTimeout(resetCopiedTimeoutRef.current);
resetCopiedTimeoutRef.current = undefined;
}
setCopied(false);
if (!normalized) {
setDisplay("Erro");
setAccumulator(null);
setOperator(null);
setOverwrite(true);
setHistory(null);
return;
}
// Limitar a 10 dígitos
const digitCount = normalized.replace(/[-.]/g, "").length;
if (digitCount > 10) {
setDisplay("Erro");
setAccumulator(null);
setOperator(null);
setOverwrite(true);
setHistory(null);
return;
}
setDisplay(normalized);
setAccumulator(null);
setOperator(null);
setOverwrite(false);
setHistory(null);
} catch (error) {
console.error("Não foi possível colar o valor na calculadora.", error);
}
};
useEffect(() => {
return () => {
if (resetCopiedTimeoutRef.current !== undefined) {
window.clearTimeout(resetCopiedTimeoutRef.current);
}
};
}, []);
return {
display,
operator,
expression,
history,
resultText,
copied,
buttons,
inputDigit,
inputDecimal,
setNextOperator,
evaluate,
deleteLastDigit,
reset,
applyPercent,
copyToClipboard,
pasteFromClipboard,
};
}