refactor: migrate from ESLint to Biome and extract SQL queries to data.ts
- Replace ESLint with Biome for linting and formatting - Configure Biome with tabs, double quotes, and organized imports - Move all SQL/Drizzle queries from page.tsx files to data.ts files - Create new data.ts files for: ajustes, dashboard, relatorios/categorias - Update existing data.ts files: extrato, fatura (add lancamentos queries) - Remove all drizzle-orm imports from page.tsx files - Update README.md with new tooling info Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,94 +1,92 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
type UseCalculatorKeyboardParams = {
|
||||
canCopy: boolean;
|
||||
onCopy: () => void | Promise<void>;
|
||||
onPaste: () => void | Promise<void>;
|
||||
canCopy: boolean;
|
||||
onCopy: () => void | Promise<void>;
|
||||
onPaste: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
function shouldIgnoreForEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!target || !(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
if (!target || !(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tagName = target.tagName;
|
||||
return (
|
||||
tagName === "INPUT" ||
|
||||
tagName === "TEXTAREA" ||
|
||||
target.isContentEditable
|
||||
);
|
||||
const tagName = target.tagName;
|
||||
return (
|
||||
tagName === "INPUT" || tagName === "TEXTAREA" || target.isContentEditable
|
||||
);
|
||||
}
|
||||
|
||||
export function useCalculatorKeyboard({
|
||||
canCopy,
|
||||
onCopy,
|
||||
onPaste,
|
||||
canCopy,
|
||||
onCopy,
|
||||
onPaste,
|
||||
}: UseCalculatorKeyboardParams) {
|
||||
useEffect(() => {
|
||||
if (!canCopy) {
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!canCopy) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
return;
|
||||
}
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldIgnoreForEditableTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
if (shouldIgnoreForEditableTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() !== "c") {
|
||||
return;
|
||||
}
|
||||
if (event.key.toLowerCase() !== "c") {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().trim().length > 0) {
|
||||
return;
|
||||
}
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().trim().length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
void onCopy();
|
||||
};
|
||||
event.preventDefault();
|
||||
void onCopy();
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [canCopy, onCopy]);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [canCopy, onCopy]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePasteShortcut = (event: KeyboardEvent) => {
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
const handlePasteShortcut = (event: KeyboardEvent) => {
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() !== "v") {
|
||||
return;
|
||||
}
|
||||
if (event.key.toLowerCase() !== "v") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldIgnoreForEditableTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
if (shouldIgnoreForEditableTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().trim().length > 0) {
|
||||
return;
|
||||
}
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().trim().length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!navigator.clipboard?.readText) {
|
||||
return;
|
||||
}
|
||||
if (!navigator.clipboard?.readText) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
void onPaste();
|
||||
};
|
||||
event.preventDefault();
|
||||
void onPaste();
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handlePasteShortcut);
|
||||
document.addEventListener("keydown", handlePasteShortcut);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handlePasteShortcut);
|
||||
};
|
||||
}, [onPaste]);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handlePasteShortcut);
|
||||
};
|
||||
}, [onPaste]);
|
||||
}
|
||||
|
||||
@@ -1,384 +1,386 @@
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type { buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
OPERATOR_SYMBOLS,
|
||||
formatLocaleValue,
|
||||
formatNumber,
|
||||
normalizeClipboardNumber,
|
||||
performOperation,
|
||||
type Operator,
|
||||
formatLocaleValue,
|
||||
formatNumber,
|
||||
normalizeClipboardNumber,
|
||||
OPERATOR_SYMBOLS,
|
||||
type Operator,
|
||||
performOperation,
|
||||
} from "@/lib/utils/calculator";
|
||||
import { type buttonVariants } from "@/components/ui/button";
|
||||
import { type VariantProps } from "class-variance-authority";
|
||||
|
||||
export type CalculatorButtonConfig = {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: VariantProps<typeof buttonVariants>["variant"];
|
||||
colSpan?: number;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: VariantProps<typeof buttonVariants>["variant"];
|
||||
colSpan?: number;
|
||||
};
|
||||
|
||||
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 [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 = useMemo(() => Number(display), [display]);
|
||||
const currentValue = useMemo(() => Number(display), [display]);
|
||||
|
||||
const resultText = useMemo(() => {
|
||||
if (display === "Erro") {
|
||||
return null;
|
||||
}
|
||||
const resultText = useMemo(() => {
|
||||
if (display === "Erro") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = formatNumber(currentValue);
|
||||
if (normalized === "Erro") {
|
||||
return null;
|
||||
}
|
||||
const normalized = formatNumber(currentValue);
|
||||
if (normalized === "Erro") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return formatLocaleValue(normalized);
|
||||
}, [currentValue, display]);
|
||||
return formatLocaleValue(normalized);
|
||||
}, [currentValue, display]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setDisplay("0");
|
||||
setAccumulator(null);
|
||||
setOperator(null);
|
||||
setOverwrite(false);
|
||||
setHistory(null);
|
||||
}, []);
|
||||
const reset = useCallback(() => {
|
||||
setDisplay("0");
|
||||
setAccumulator(null);
|
||||
setOperator(null);
|
||||
setOverwrite(false);
|
||||
setHistory(null);
|
||||
}, []);
|
||||
|
||||
const inputDigit = useCallback(
|
||||
(digit: string) => {
|
||||
// Check conditions before state updates
|
||||
const shouldReset = overwrite || display === "Erro";
|
||||
const inputDigit = useCallback(
|
||||
(digit: string) => {
|
||||
// Check conditions before state updates
|
||||
const shouldReset = overwrite || display === "Erro";
|
||||
|
||||
setDisplay((prev) => {
|
||||
if (shouldReset) {
|
||||
return digit;
|
||||
}
|
||||
setDisplay((prev) => {
|
||||
if (shouldReset) {
|
||||
return digit;
|
||||
}
|
||||
|
||||
if (prev === "0") {
|
||||
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;
|
||||
}
|
||||
// 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}`;
|
||||
});
|
||||
return `${prev}${digit}`;
|
||||
});
|
||||
|
||||
// Update related states after display update
|
||||
if (shouldReset) {
|
||||
setOverwrite(false);
|
||||
setHistory(null);
|
||||
}
|
||||
},
|
||||
[overwrite, display]
|
||||
);
|
||||
// Update related states after display update
|
||||
if (shouldReset) {
|
||||
setOverwrite(false);
|
||||
setHistory(null);
|
||||
}
|
||||
},
|
||||
[overwrite, display],
|
||||
);
|
||||
|
||||
const inputDecimal = useCallback(() => {
|
||||
// Check conditions before state updates
|
||||
const shouldReset = overwrite || display === "Erro";
|
||||
const inputDecimal = useCallback(() => {
|
||||
// Check conditions before state updates
|
||||
const shouldReset = overwrite || display === "Erro";
|
||||
|
||||
setDisplay((prev) => {
|
||||
if (shouldReset) {
|
||||
return "0.";
|
||||
}
|
||||
setDisplay((prev) => {
|
||||
if (shouldReset) {
|
||||
return "0.";
|
||||
}
|
||||
|
||||
if (prev.includes(".")) {
|
||||
return prev;
|
||||
}
|
||||
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;
|
||||
}
|
||||
// Limitar a 10 dígitos antes de adicionar o ponto decimal
|
||||
const digitCount = prev.replace(/[-]/g, "").length;
|
||||
if (digitCount >= 10) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return `${prev}.`;
|
||||
});
|
||||
return `${prev}.`;
|
||||
});
|
||||
|
||||
// Update related states after display update
|
||||
if (shouldReset) {
|
||||
setOverwrite(false);
|
||||
setHistory(null);
|
||||
}
|
||||
}, [overwrite, display]);
|
||||
// Update related states after display update
|
||||
if (shouldReset) {
|
||||
setOverwrite(false);
|
||||
setHistory(null);
|
||||
}
|
||||
}, [overwrite, display]);
|
||||
|
||||
const setNextOperator = useCallback(
|
||||
(nextOperator: Operator) => {
|
||||
if (display === "Erro") {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
const setNextOperator = useCallback(
|
||||
(nextOperator: Operator) => {
|
||||
if (display === "Erro") {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const value = currentValue;
|
||||
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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
},
|
||||
[accumulator, currentValue, display, operator, overwrite, reset]
|
||||
);
|
||||
setOperator(nextOperator);
|
||||
setOverwrite(true);
|
||||
setHistory(null);
|
||||
},
|
||||
[accumulator, currentValue, display, operator, overwrite, reset],
|
||||
);
|
||||
|
||||
const evaluate = useCallback(() => {
|
||||
if (operator === null || accumulator === null || display === "Erro") {
|
||||
return;
|
||||
}
|
||||
const evaluate = useCallback(() => {
|
||||
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);
|
||||
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);
|
||||
}, [accumulator, currentValue, display, operator]);
|
||||
setDisplay(formatted);
|
||||
setAccumulator(Number.isFinite(result) ? result : null);
|
||||
setOperator(null);
|
||||
setOverwrite(true);
|
||||
setHistory(operation);
|
||||
}, [accumulator, currentValue, display, operator]);
|
||||
|
||||
const toggleSign = useCallback(() => {
|
||||
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);
|
||||
}
|
||||
}, [overwrite]);
|
||||
const toggleSign = useCallback(() => {
|
||||
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);
|
||||
}
|
||||
}, [overwrite]);
|
||||
|
||||
const deleteLastDigit = useCallback(() => {
|
||||
setHistory(null);
|
||||
const deleteLastDigit = useCallback(() => {
|
||||
setHistory(null);
|
||||
|
||||
// Check conditions before state updates
|
||||
const isError = display === "Erro";
|
||||
// Check conditions before state updates
|
||||
const isError = display === "Erro";
|
||||
|
||||
setDisplay((prev) => {
|
||||
if (prev === "Erro") {
|
||||
return "0";
|
||||
}
|
||||
setDisplay((prev) => {
|
||||
if (prev === "Erro") {
|
||||
return "0";
|
||||
}
|
||||
|
||||
if (overwrite) {
|
||||
return "0";
|
||||
}
|
||||
if (overwrite) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
if (prev.length <= 1 || (prev.length === 2 && prev.startsWith("-"))) {
|
||||
return "0";
|
||||
}
|
||||
if (prev.length <= 1 || (prev.length === 2 && prev.startsWith("-"))) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
return prev.slice(0, -1);
|
||||
});
|
||||
return prev.slice(0, -1);
|
||||
});
|
||||
|
||||
// Update related states after display update
|
||||
if (isError) {
|
||||
setAccumulator(null);
|
||||
setOperator(null);
|
||||
setOverwrite(false);
|
||||
} else if (overwrite) {
|
||||
setOverwrite(false);
|
||||
}
|
||||
}, [overwrite, display]);
|
||||
// Update related states after display update
|
||||
if (isError) {
|
||||
setAccumulator(null);
|
||||
setOperator(null);
|
||||
setOverwrite(false);
|
||||
} else if (overwrite) {
|
||||
setOverwrite(false);
|
||||
}
|
||||
}, [overwrite, display]);
|
||||
|
||||
const applyPercent = useCallback(() => {
|
||||
setDisplay((prev) => {
|
||||
if (prev === "Erro") {
|
||||
return prev;
|
||||
}
|
||||
const value = Number(prev);
|
||||
return formatNumber(value / 100);
|
||||
});
|
||||
setOverwrite(true);
|
||||
setHistory(null);
|
||||
}, []);
|
||||
const applyPercent = useCallback(() => {
|
||||
setDisplay((prev) => {
|
||||
if (prev === "Erro") {
|
||||
return prev;
|
||||
}
|
||||
const value = Number(prev);
|
||||
return formatNumber(value / 100);
|
||||
});
|
||||
setOverwrite(true);
|
||||
setHistory(null);
|
||||
}, []);
|
||||
|
||||
const expression = useMemo(() => {
|
||||
if (display === "Erro") {
|
||||
return "Erro";
|
||||
}
|
||||
const expression = useMemo(() => {
|
||||
if (display === "Erro") {
|
||||
return "Erro";
|
||||
}
|
||||
|
||||
if (operator && accumulator !== null) {
|
||||
const symbol = OPERATOR_SYMBOLS[operator];
|
||||
const left = formatLocaleValue(formatNumber(accumulator));
|
||||
if (operator && accumulator !== null) {
|
||||
const symbol = OPERATOR_SYMBOLS[operator];
|
||||
const left = formatLocaleValue(formatNumber(accumulator));
|
||||
|
||||
if (overwrite) {
|
||||
return `${left} ${symbol}`;
|
||||
}
|
||||
if (overwrite) {
|
||||
return `${left} ${symbol}`;
|
||||
}
|
||||
|
||||
return `${left} ${symbol} ${formatLocaleValue(display)}`;
|
||||
}
|
||||
return `${left} ${symbol} ${formatLocaleValue(display)}`;
|
||||
}
|
||||
|
||||
return formatLocaleValue(display);
|
||||
}, [accumulator, display, operator, overwrite]);
|
||||
return formatLocaleValue(display);
|
||||
}, [accumulator, display, operator, overwrite]);
|
||||
|
||||
const buttons = useMemo(() => {
|
||||
const makeOperatorHandler = (nextOperator: Operator) => () =>
|
||||
setNextOperator(nextOperator);
|
||||
const buttons = useMemo(() => {
|
||||
const makeOperatorHandler = (nextOperator: Operator) => () =>
|
||||
setNextOperator(nextOperator);
|
||||
|
||||
return [
|
||||
[
|
||||
{ label: "C", onClick: reset, variant: "destructive" },
|
||||
{ label: "⌫", onClick: deleteLastDigit, variant: "default" },
|
||||
{ label: "%", onClick: applyPercent, variant: "default" },
|
||||
{
|
||||
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: "default" },
|
||||
{ label: "0", onClick: () => inputDigit("0") },
|
||||
{ label: ",", onClick: inputDecimal },
|
||||
{ label: "=", onClick: evaluate, variant: "default" },
|
||||
],
|
||||
] satisfies CalculatorButtonConfig[][];
|
||||
}, [
|
||||
applyPercent,
|
||||
deleteLastDigit,
|
||||
evaluate,
|
||||
inputDecimal,
|
||||
inputDigit,
|
||||
reset,
|
||||
setNextOperator,
|
||||
toggleSign,
|
||||
]);
|
||||
return [
|
||||
[
|
||||
{ label: "C", onClick: reset, variant: "destructive" },
|
||||
{ label: "⌫", onClick: deleteLastDigit, variant: "default" },
|
||||
{ label: "%", onClick: applyPercent, variant: "default" },
|
||||
{
|
||||
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: "default" },
|
||||
{ label: "0", onClick: () => inputDigit("0") },
|
||||
{ label: ",", onClick: inputDecimal },
|
||||
{ label: "=", onClick: evaluate, variant: "default" },
|
||||
],
|
||||
] satisfies CalculatorButtonConfig[][];
|
||||
}, [
|
||||
applyPercent,
|
||||
deleteLastDigit,
|
||||
evaluate,
|
||||
inputDecimal,
|
||||
inputDigit,
|
||||
reset,
|
||||
setNextOperator,
|
||||
toggleSign,
|
||||
]);
|
||||
|
||||
const copyToClipboard = useCallback(async () => {
|
||||
if (!resultText) return;
|
||||
const copyToClipboard = useCallback(async () => {
|
||||
if (!resultText) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(resultText);
|
||||
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);
|
||||
}
|
||||
}, [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,
|
||||
);
|
||||
}
|
||||
}, [resultText]);
|
||||
|
||||
const pasteFromClipboard = useCallback(async () => {
|
||||
if (!navigator.clipboard?.readText) return;
|
||||
const pasteFromClipboard = useCallback(async () => {
|
||||
if (!navigator.clipboard?.readText) return;
|
||||
|
||||
try {
|
||||
const rawValue = await navigator.clipboard.readText();
|
||||
const normalized = normalizeClipboardNumber(rawValue);
|
||||
try {
|
||||
const rawValue = await navigator.clipboard.readText();
|
||||
const normalized = normalizeClipboardNumber(rawValue);
|
||||
|
||||
if (resetCopiedTimeoutRef.current !== undefined) {
|
||||
window.clearTimeout(resetCopiedTimeoutRef.current);
|
||||
resetCopiedTimeoutRef.current = undefined;
|
||||
}
|
||||
if (resetCopiedTimeoutRef.current !== undefined) {
|
||||
window.clearTimeout(resetCopiedTimeoutRef.current);
|
||||
resetCopiedTimeoutRef.current = undefined;
|
||||
}
|
||||
|
||||
setCopied(false);
|
||||
setCopied(false);
|
||||
|
||||
if (!normalized) {
|
||||
setDisplay("Erro");
|
||||
setAccumulator(null);
|
||||
setOperator(null);
|
||||
setOverwrite(true);
|
||||
setHistory(null);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}, []);
|
||||
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);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (resetCopiedTimeoutRef.current !== undefined) {
|
||||
window.clearTimeout(resetCopiedTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
expression,
|
||||
history,
|
||||
resultText,
|
||||
copied,
|
||||
buttons,
|
||||
copyToClipboard,
|
||||
pasteFromClipboard,
|
||||
};
|
||||
return {
|
||||
expression,
|
||||
history,
|
||||
resultText,
|
||||
copied,
|
||||
buttons,
|
||||
copyToClipboard,
|
||||
pasteFromClipboard,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,34 +18,34 @@ import { useCallback, useEffect, useState } from "react";
|
||||
* ```
|
||||
*/
|
||||
export function useControlledState<T>(
|
||||
controlledValue: T | undefined,
|
||||
defaultValue: T,
|
||||
onChange?: (value: T) => void
|
||||
controlledValue: T | undefined,
|
||||
defaultValue: T,
|
||||
onChange?: (value: T) => void,
|
||||
): [T, (value: T) => void] {
|
||||
const [internalValue, setInternalValue] = useState<T>(defaultValue);
|
||||
const [internalValue, setInternalValue] = useState<T>(defaultValue);
|
||||
|
||||
// Sync internal value when controlled value changes
|
||||
useEffect(() => {
|
||||
if (controlledValue !== undefined) {
|
||||
setInternalValue(controlledValue);
|
||||
}
|
||||
}, [controlledValue]);
|
||||
// Sync internal value when controlled value changes
|
||||
useEffect(() => {
|
||||
if (controlledValue !== undefined) {
|
||||
setInternalValue(controlledValue);
|
||||
}
|
||||
}, [controlledValue]);
|
||||
|
||||
// Use controlled value if provided, otherwise use internal value
|
||||
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
||||
// Use controlled value if provided, otherwise use internal value
|
||||
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
||||
|
||||
const setValue = useCallback(
|
||||
(newValue: T) => {
|
||||
// Update internal state if uncontrolled
|
||||
if (controlledValue === undefined) {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
const setValue = useCallback(
|
||||
(newValue: T) => {
|
||||
// Update internal state if uncontrolled
|
||||
if (controlledValue === undefined) {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
|
||||
// Always call onChange if provided
|
||||
onChange?.(newValue);
|
||||
},
|
||||
[controlledValue, onChange]
|
||||
);
|
||||
// Always call onChange if provided
|
||||
onChange?.(newValue);
|
||||
},
|
||||
[controlledValue, onChange],
|
||||
);
|
||||
|
||||
return [value, setValue];
|
||||
return [value, setValue];
|
||||
}
|
||||
|
||||
@@ -16,40 +16,38 @@ import { useCallback, useState } from "react";
|
||||
* updateField('name', 'John');
|
||||
* ```
|
||||
*/
|
||||
export function useFormState<T extends Record<string, any>>(
|
||||
initialValues: T
|
||||
) {
|
||||
const [formState, setFormState] = useState<T>(initialValues);
|
||||
export function useFormState<T extends Record<string, any>>(initialValues: T) {
|
||||
const [formState, setFormState] = useState<T>(initialValues);
|
||||
|
||||
/**
|
||||
* Updates a single field in the form state
|
||||
*/
|
||||
const updateField = useCallback(<K extends keyof T>(
|
||||
field: K,
|
||||
value: T[K]
|
||||
) => {
|
||||
setFormState((prev) => ({ ...prev, [field]: value }));
|
||||
}, []);
|
||||
/**
|
||||
* Updates a single field in the form state
|
||||
*/
|
||||
const updateField = useCallback(
|
||||
<K extends keyof T>(field: K, value: T[K]) => {
|
||||
setFormState((prev) => ({ ...prev, [field]: value }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Resets form to initial values
|
||||
*/
|
||||
const resetForm = useCallback(() => {
|
||||
setFormState(initialValues);
|
||||
}, [initialValues]);
|
||||
/**
|
||||
* Resets form to initial values
|
||||
*/
|
||||
const resetForm = useCallback(() => {
|
||||
setFormState(initialValues);
|
||||
}, [initialValues]);
|
||||
|
||||
/**
|
||||
* Updates multiple fields at once
|
||||
*/
|
||||
const updateFields = useCallback((updates: Partial<T>) => {
|
||||
setFormState((prev) => ({ ...prev, ...updates }));
|
||||
}, []);
|
||||
/**
|
||||
* Updates multiple fields at once
|
||||
*/
|
||||
const updateFields = useCallback((updates: Partial<T>) => {
|
||||
setFormState((prev) => ({ ...prev, ...updates }));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
formState,
|
||||
updateField,
|
||||
updateFields,
|
||||
resetForm,
|
||||
setFormState,
|
||||
};
|
||||
return {
|
||||
formState,
|
||||
updateField,
|
||||
updateFields,
|
||||
resetForm,
|
||||
setFormState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { useCallback } from "react";
|
||||
import { deriveNameFromLogo } from "@/lib/logo";
|
||||
|
||||
interface UseLogoSelectionProps {
|
||||
mode: "create" | "update";
|
||||
currentLogo: string;
|
||||
currentName: string;
|
||||
onUpdate: (updates: { logo: string; name?: string }) => void;
|
||||
mode: "create" | "update";
|
||||
currentLogo: string;
|
||||
currentName: string;
|
||||
onUpdate: (updates: { logo: string; name?: string }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,29 +30,29 @@ interface UseLogoSelectionProps {
|
||||
* ```
|
||||
*/
|
||||
export function useLogoSelection({
|
||||
mode,
|
||||
currentLogo,
|
||||
currentName,
|
||||
onUpdate,
|
||||
mode,
|
||||
currentLogo,
|
||||
currentName,
|
||||
onUpdate,
|
||||
}: UseLogoSelectionProps) {
|
||||
const handleLogoSelection = useCallback(
|
||||
(newLogo: string) => {
|
||||
const derived = deriveNameFromLogo(newLogo);
|
||||
const previousDerived = deriveNameFromLogo(currentLogo);
|
||||
const handleLogoSelection = useCallback(
|
||||
(newLogo: string) => {
|
||||
const derived = deriveNameFromLogo(newLogo);
|
||||
const previousDerived = deriveNameFromLogo(currentLogo);
|
||||
|
||||
const shouldUpdateName =
|
||||
mode === "create" ||
|
||||
currentName.trim().length === 0 ||
|
||||
previousDerived === currentName.trim();
|
||||
const shouldUpdateName =
|
||||
mode === "create" ||
|
||||
currentName.trim().length === 0 ||
|
||||
previousDerived === currentName.trim();
|
||||
|
||||
if (shouldUpdateName) {
|
||||
onUpdate({ logo: newLogo, name: derived });
|
||||
} else {
|
||||
onUpdate({ logo: newLogo });
|
||||
}
|
||||
},
|
||||
[mode, currentLogo, currentName, onUpdate]
|
||||
);
|
||||
if (shouldUpdateName) {
|
||||
onUpdate({ logo: newLogo, name: derived });
|
||||
} else {
|
||||
onUpdate({ logo: newLogo });
|
||||
}
|
||||
},
|
||||
[mode, currentLogo, currentName, onUpdate],
|
||||
);
|
||||
|
||||
return handleLogoSelection;
|
||||
return handleLogoSelection;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile
|
||||
return !!isMobile;
|
||||
}
|
||||
|
||||
@@ -10,76 +10,76 @@ const PERIOD_PARAM = "periodo";
|
||||
const normalizeMonth = (value: string) => value.trim().toLowerCase();
|
||||
|
||||
export function useMonthPeriod() {
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
// Get current date info
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
const currentMonthName = MONTH_NAMES[now.getMonth()];
|
||||
const optionsMeses = [...MONTH_NAMES];
|
||||
// Get current date info
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
const currentMonthName = MONTH_NAMES[now.getMonth()];
|
||||
const optionsMeses = [...MONTH_NAMES];
|
||||
|
||||
const defaultMonth = currentMonthName;
|
||||
const defaultYear = currentYear.toString();
|
||||
const defaultMonth = currentMonthName;
|
||||
const defaultYear = currentYear.toString();
|
||||
|
||||
const periodFromParams = searchParams.get(PERIOD_PARAM);
|
||||
const periodFromParams = searchParams.get(PERIOD_PARAM);
|
||||
|
||||
const { month: currentMonth, year: currentYearValue } = useMemo(() => {
|
||||
if (!periodFromParams) {
|
||||
return { month: defaultMonth, year: defaultYear };
|
||||
}
|
||||
const { month: currentMonth, year: currentYearValue } = useMemo(() => {
|
||||
if (!periodFromParams) {
|
||||
return { month: defaultMonth, year: defaultYear };
|
||||
}
|
||||
|
||||
const [rawMonth, rawYear] = periodFromParams.split("-");
|
||||
const normalizedMonth = normalizeMonth(rawMonth ?? "");
|
||||
const normalizedYear = (rawYear ?? "").trim();
|
||||
const monthExists = optionsMeses.includes(normalizedMonth);
|
||||
const parsedYear = Number.parseInt(normalizedYear, 10);
|
||||
const [rawMonth, rawYear] = periodFromParams.split("-");
|
||||
const normalizedMonth = normalizeMonth(rawMonth ?? "");
|
||||
const normalizedYear = (rawYear ?? "").trim();
|
||||
const monthExists = optionsMeses.includes(normalizedMonth);
|
||||
const parsedYear = Number.parseInt(normalizedYear, 10);
|
||||
|
||||
if (!monthExists || Number.isNaN(parsedYear)) {
|
||||
return { month: defaultMonth, year: defaultYear };
|
||||
}
|
||||
if (!monthExists || Number.isNaN(parsedYear)) {
|
||||
return { month: defaultMonth, year: defaultYear };
|
||||
}
|
||||
|
||||
return {
|
||||
month: normalizedMonth,
|
||||
year: parsedYear.toString(),
|
||||
};
|
||||
}, [periodFromParams, defaultMonth, defaultYear, optionsMeses]);
|
||||
return {
|
||||
month: normalizedMonth,
|
||||
year: parsedYear.toString(),
|
||||
};
|
||||
}, [periodFromParams, defaultMonth, defaultYear, optionsMeses]);
|
||||
|
||||
const buildHref = useCallback(
|
||||
(month: string, year: string | number) => {
|
||||
const normalizedMonth = normalizeMonth(month);
|
||||
const normalizedYear = String(year).trim();
|
||||
const buildHref = useCallback(
|
||||
(month: string, year: string | number) => {
|
||||
const normalizedMonth = normalizeMonth(month);
|
||||
const normalizedYear = String(year).trim();
|
||||
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(PERIOD_PARAM, `${normalizedMonth}-${normalizedYear}`);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(PERIOD_PARAM, `${normalizedMonth}-${normalizedYear}`);
|
||||
|
||||
return `${pathname}?${params.toString()}`;
|
||||
},
|
||||
[pathname, searchParams]
|
||||
);
|
||||
return `${pathname}?${params.toString()}`;
|
||||
},
|
||||
[pathname, searchParams],
|
||||
);
|
||||
|
||||
const replacePeriod = useCallback(
|
||||
(target: string) => {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const replacePeriod = useCallback(
|
||||
(target: string) => {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.replace(target, { scroll: false });
|
||||
},
|
||||
[router]
|
||||
);
|
||||
router.replace(target, { scroll: false });
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
return {
|
||||
monthNames: optionsMeses,
|
||||
pathname,
|
||||
currentMonth,
|
||||
currentYear: currentYearValue,
|
||||
defaultMonth,
|
||||
defaultYear,
|
||||
buildHref,
|
||||
replacePeriod,
|
||||
};
|
||||
return {
|
||||
monthNames: optionsMeses,
|
||||
pathname,
|
||||
currentMonth,
|
||||
currentYear: currentYearValue,
|
||||
defaultMonth,
|
||||
defaultYear,
|
||||
buildHref,
|
||||
replacePeriod,
|
||||
};
|
||||
}
|
||||
|
||||
export { PERIOD_PARAM as MONTH_PERIOD_PARAM };
|
||||
|
||||
Reference in New Issue
Block a user