refactor hooks organization and month picker
This commit is contained in:
@@ -16,8 +16,8 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useDraggableDialog } from "@/hooks/use-draggable-dialog";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import { useDraggableDialog } from "./use-draggable-dialog";
|
||||
|
||||
type Variant = React.ComponentProps<typeof Button>["variant"];
|
||||
type Size = React.ComponentProps<typeof Button>["size"];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CalculatorButtonConfig } from "@/components/calculadora/use-calculator-state";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { CalculatorButtonConfig } from "@/hooks/use-calculator-state";
|
||||
import type { Operator } from "@/lib/utils/calculator";
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { CalculatorKeypad } from "@/components/calculadora/calculator-keypad";
|
||||
import { useCalculatorKeyboard } from "@/components/calculadora/use-calculator-keyboard";
|
||||
import { useCalculatorState } from "@/components/calculadora/use-calculator-state";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useCalculatorKeyboard } from "@/hooks/use-calculator-keyboard";
|
||||
import { useCalculatorState } from "@/hooks/use-calculator-state";
|
||||
import { CalculatorDisplay } from "./calculator-display";
|
||||
|
||||
type CalculatorProps = {
|
||||
|
||||
143
components/calculadora/use-calculator-keyboard.ts
Normal file
143
components/calculadora/use-calculator-keyboard.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useEffect } from "react";
|
||||
import type { Operator } from "@/lib/utils/calculator";
|
||||
|
||||
type UseCalculatorKeyboardParams = {
|
||||
isOpen: boolean;
|
||||
canCopy: boolean;
|
||||
onCopy: () => void | Promise<void>;
|
||||
onPaste: () => void | Promise<void>;
|
||||
inputDigit: (digit: string) => void;
|
||||
inputDecimal: () => void;
|
||||
setNextOperator: (op: Operator) => void;
|
||||
evaluate: () => void;
|
||||
deleteLastDigit: () => void;
|
||||
reset: () => void;
|
||||
applyPercent: () => void;
|
||||
};
|
||||
|
||||
function shouldIgnoreForEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!target || !(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tagName = target.tagName;
|
||||
return (
|
||||
tagName === "INPUT" || tagName === "TEXTAREA" || target.isContentEditable
|
||||
);
|
||||
}
|
||||
|
||||
const KEY_TO_OPERATOR: Record<string, Operator> = {
|
||||
"+": "add",
|
||||
"-": "subtract",
|
||||
"*": "multiply",
|
||||
"/": "divide",
|
||||
};
|
||||
|
||||
export function useCalculatorKeyboard({
|
||||
isOpen,
|
||||
canCopy,
|
||||
onCopy,
|
||||
onPaste,
|
||||
inputDigit,
|
||||
inputDecimal,
|
||||
setNextOperator,
|
||||
evaluate,
|
||||
deleteLastDigit,
|
||||
reset,
|
||||
applyPercent,
|
||||
}: UseCalculatorKeyboardParams) {
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const { key, ctrlKey, metaKey } = event;
|
||||
|
||||
// Ctrl/Cmd shortcuts
|
||||
if (ctrlKey || metaKey) {
|
||||
if (shouldIgnoreForEditableTarget(event.target)) return;
|
||||
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (lowerKey === "c" && canCopy) {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().trim().length > 0) return;
|
||||
event.preventDefault();
|
||||
void onCopy();
|
||||
} else if (lowerKey === "v") {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().trim().length > 0) return;
|
||||
if (!navigator.clipboard?.readText) return;
|
||||
event.preventDefault();
|
||||
void onPaste();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Digits
|
||||
if (key >= "0" && key <= "9") {
|
||||
event.preventDefault();
|
||||
inputDigit(key);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decimal
|
||||
if (key === "." || key === ",") {
|
||||
event.preventDefault();
|
||||
inputDecimal();
|
||||
return;
|
||||
}
|
||||
|
||||
// Operators
|
||||
const op = KEY_TO_OPERATOR[key];
|
||||
if (op) {
|
||||
event.preventDefault();
|
||||
setNextOperator(op);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evaluate
|
||||
if (key === "Enter" || key === "=") {
|
||||
event.preventDefault();
|
||||
evaluate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Backspace
|
||||
if (key === "Backspace") {
|
||||
event.preventDefault();
|
||||
deleteLastDigit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape resets calculator (dialog close is handled by onEscapeKeyDown)
|
||||
if (key === "Escape") {
|
||||
event.preventDefault();
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// Percent
|
||||
if (key === "%") {
|
||||
event.preventDefault();
|
||||
applyPercent();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [
|
||||
isOpen,
|
||||
canCopy,
|
||||
onCopy,
|
||||
onPaste,
|
||||
inputDigit,
|
||||
inputDecimal,
|
||||
setNextOperator,
|
||||
evaluate,
|
||||
deleteLastDigit,
|
||||
reset,
|
||||
applyPercent,
|
||||
]);
|
||||
}
|
||||
379
components/calculadora/use-calculator-state.ts
Normal file
379
components/calculadora/use-calculator-state.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
112
components/calculadora/use-draggable-dialog.ts
Normal file
112
components/calculadora/use-draggable-dialog.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
|
||||
type Position = { x: number; y: number };
|
||||
|
||||
const MIN_VISIBLE_PX = 20;
|
||||
|
||||
function clampPosition(
|
||||
x: number,
|
||||
y: number,
|
||||
elementWidth: number,
|
||||
elementHeight: number,
|
||||
): Position {
|
||||
// Dialog starts centered (left/top 50% + translate(-50%, -50%)).
|
||||
// Clamp offsets so at least MIN_VISIBLE_PX remains visible on each axis.
|
||||
const halfViewportWidth = window.innerWidth / 2;
|
||||
const halfViewportHeight = window.innerHeight / 2;
|
||||
const halfElementWidth = elementWidth / 2;
|
||||
const halfElementHeight = elementHeight / 2;
|
||||
|
||||
const minX = MIN_VISIBLE_PX - (halfViewportWidth + halfElementWidth);
|
||||
const maxX = halfViewportWidth + halfElementWidth - MIN_VISIBLE_PX;
|
||||
const minY = MIN_VISIBLE_PX - (halfViewportHeight + halfElementHeight);
|
||||
const maxY = halfViewportHeight + halfElementHeight - MIN_VISIBLE_PX;
|
||||
|
||||
return {
|
||||
x: Math.min(Math.max(x, minX), maxX),
|
||||
y: Math.min(Math.max(y, minY), maxY),
|
||||
};
|
||||
}
|
||||
|
||||
function applyPosition(el: HTMLElement, x: number, y: number) {
|
||||
if (x === 0 && y === 0) {
|
||||
el.style.translate = "";
|
||||
el.style.transform = "";
|
||||
} else {
|
||||
// Keep the dialog's centered baseline (-50%, -50%) and only add drag offset.
|
||||
el.style.translate = `calc(-50% + ${x}px) calc(-50% + ${y}px)`;
|
||||
el.style.transform = "";
|
||||
}
|
||||
}
|
||||
|
||||
export function useDraggableDialog() {
|
||||
const offset = useRef<Position>({ x: 0, y: 0 });
|
||||
const dragStart = useRef<Position | null>(null);
|
||||
const initialOffset = useRef<Position>({ x: 0, y: 0 });
|
||||
const contentRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const onPointerDown = useCallback((e: React.PointerEvent<HTMLElement>) => {
|
||||
if (e.button !== 0) return;
|
||||
|
||||
dragStart.current = { x: e.clientX, y: e.clientY };
|
||||
initialOffset.current = { x: offset.current.x, y: offset.current.y };
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
}, []);
|
||||
|
||||
const onPointerMove = useCallback((e: React.PointerEvent<HTMLElement>) => {
|
||||
if (!dragStart.current || !contentRef.current) return;
|
||||
|
||||
const dx = e.clientX - dragStart.current.x;
|
||||
const dy = e.clientY - dragStart.current.y;
|
||||
|
||||
const rawX = initialOffset.current.x + dx;
|
||||
const rawY = initialOffset.current.y + dy;
|
||||
|
||||
const el = contentRef.current;
|
||||
const clamped = clampPosition(rawX, rawY, el.offsetWidth, el.offsetHeight);
|
||||
|
||||
offset.current = clamped;
|
||||
applyPosition(el, clamped.x, clamped.y);
|
||||
}, []);
|
||||
|
||||
const onPointerUp = useCallback((e: React.PointerEvent<HTMLElement>) => {
|
||||
dragStart.current = null;
|
||||
if ((e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) {
|
||||
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onPointerCancel = useCallback(() => {
|
||||
dragStart.current = null;
|
||||
}, []);
|
||||
|
||||
const onLostPointerCapture = useCallback(() => {
|
||||
dragStart.current = null;
|
||||
}, []);
|
||||
|
||||
const resetPosition = useCallback(() => {
|
||||
offset.current = { x: 0, y: 0 };
|
||||
if (contentRef.current) {
|
||||
applyPosition(contentRef.current, 0, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const dragHandleProps = {
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp,
|
||||
onPointerCancel,
|
||||
onLostPointerCapture,
|
||||
style: { touchAction: "none" as const, cursor: "grab" },
|
||||
};
|
||||
|
||||
const contentRefCallback = useCallback((node: HTMLElement | null) => {
|
||||
contentRef.current = node;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
dragHandleProps,
|
||||
contentRefCallback,
|
||||
resetPosition,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user