Files
personal-dashboard/src/components/CalculatorWidget.tsx
T
Claude a4051ae132 Initial commit: Personal Dashboard
Next.js 16 dashboard with configurable widgets (favorites, notes, calendar,
clock, calculator, search, domain-check), multi-tab support, user auth,
dark mode, and Docker deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 10:02:05 +02:00

435 lines
12 KiB
TypeScript
Raw 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.
"use client";
import type { KeyboardEvent } from "react";
import { useState } from "react";
import styles from "./CalculatorWidget.module.css";
type Operator = "add" | "subtract" | "multiply" | "divide";
type CalculatorButtonProps = {
label: string;
ariaLabel?: string;
className?: string;
onClick: () => void;
};
const maxDisplayLength = 16;
function parseDisplay(value: string): number {
const normalizedValue = value.replace(",", ".");
const numberValue = Number(normalizedValue);
if (!Number.isFinite(numberValue)) {
return 0;
}
return numberValue;
}
function formatNumber(value: number): string {
if (!Number.isFinite(value)) {
return "Fehler";
}
if (Object.is(value, -0)) {
return "0";
}
const absoluteValue = Math.abs(value);
if (absoluteValue !== 0 && (absoluteValue >= 1e15 || absoluteValue < 1e-9)) {
return value.toExponential(8).replace(".", ",");
}
const roundedValue = Math.round(value * 1e10) / 1e10;
return roundedValue.toLocaleString("de-DE", {
useGrouping: false,
maximumFractionDigits: 10
});
}
function calculate(firstValue: number, secondValue: number, operator: Operator): number {
if (operator === "add") {
return firstValue + secondValue;
}
if (operator === "subtract") {
return firstValue - secondValue;
}
if (operator === "multiply") {
return firstValue * secondValue;
}
if (operator === "divide") {
if (secondValue === 0) {
return Number.NaN;
}
return firstValue / secondValue;
}
return secondValue;
}
function getOperatorLabel(operator: Operator | null): string {
if (operator === "add") {
return "+";
}
if (operator === "subtract") {
return "";
}
if (operator === "multiply") {
return "×";
}
if (operator === "divide") {
return "÷";
}
return "";
}
function CalculatorButton({ label, ariaLabel, className, onClick }: CalculatorButtonProps) {
return (
<button
type="button"
className={[styles.button, className].filter(Boolean).join(" ")}
aria-label={ariaLabel ?? label}
onClick={onClick}
>
{label}
</button>
);
}
export default function CalculatorWidget() {
const [display, setDisplay] = useState("0");
const [storedValue, setStoredValue] = useState<number | null>(null);
const [pendingOperator, setPendingOperator] = useState<Operator | null>(null);
const [waitingForOperand, setWaitingForOperand] = useState(false);
const [memory, setMemory] = useState(0);
const displayIsError = display === "Fehler";
function resetIfError(): boolean {
if (!displayIsError) {
return false;
}
setDisplay("0");
setStoredValue(null);
setPendingOperator(null);
setWaitingForOperand(false);
return true;
}
function inputDigit(digit: string) {
if (resetIfError()) {
setDisplay(digit);
return;
}
if (waitingForOperand) {
setDisplay(digit);
setWaitingForOperand(false);
return;
}
setDisplay((currentDisplay) => {
if (currentDisplay === "0") {
return digit;
}
if (currentDisplay.replace("-", "").replace(",", "").length >= maxDisplayLength) {
return currentDisplay;
}
return `${currentDisplay}${digit}`;
});
}
function inputDecimal() {
if (resetIfError()) {
setDisplay("0,");
return;
}
if (waitingForOperand) {
setDisplay("0,");
setWaitingForOperand(false);
return;
}
setDisplay((currentDisplay) => {
if (currentDisplay.includes(",")) {
return currentDisplay;
}
return `${currentDisplay},`;
});
}
function clearEntry() {
setDisplay("0");
setWaitingForOperand(false);
}
function clearAll() {
setDisplay("0");
setStoredValue(null);
setPendingOperator(null);
setWaitingForOperand(false);
}
function backspace() {
if (resetIfError() || waitingForOperand) {
setDisplay("0");
setWaitingForOperand(false);
return;
}
setDisplay((currentDisplay) => {
if (currentDisplay.length <= 1 || (currentDisplay.length === 2 && currentDisplay.startsWith("-"))) {
return "0";
}
return currentDisplay.slice(0, -1);
});
}
function toggleSign() {
if (resetIfError()) {
return;
}
setDisplay((currentDisplay) => {
if (currentDisplay === "0") {
return currentDisplay;
}
return currentDisplay.startsWith("-") ? currentDisplay.slice(1) : `-${currentDisplay}`;
});
}
function applyUnary(operation: "percent" | "reciprocal" | "square" | "sqrt") {
if (resetIfError()) {
return;
}
const currentValue = parseDisplay(display);
let nextValue = currentValue;
if (operation === "percent") {
if (storedValue !== null && pendingOperator) {
nextValue = (storedValue * currentValue) / 100;
} else {
nextValue = currentValue / 100;
}
}
if (operation === "reciprocal") {
nextValue = currentValue === 0 ? Number.NaN : 1 / currentValue;
}
if (operation === "square") {
nextValue = currentValue * currentValue;
}
if (operation === "sqrt") {
nextValue = currentValue < 0 ? Number.NaN : Math.sqrt(currentValue);
}
setDisplay(formatNumber(nextValue));
setWaitingForOperand(true);
}
function chooseOperator(operator: Operator) {
if (resetIfError()) {
return;
}
const currentValue = parseDisplay(display);
if (storedValue === null) {
setStoredValue(currentValue);
} else if (pendingOperator && !waitingForOperand) {
const result = calculate(storedValue, currentValue, pendingOperator);
setDisplay(formatNumber(result));
setStoredValue(result);
}
setPendingOperator(operator);
setWaitingForOperand(true);
}
function applyEquals() {
if (resetIfError()) {
return;
}
if (storedValue === null || pendingOperator === null) {
return;
}
const currentValue = parseDisplay(display);
const result = calculate(storedValue, currentValue, pendingOperator);
setDisplay(formatNumber(result));
setStoredValue(null);
setPendingOperator(null);
setWaitingForOperand(true);
}
function memoryClear() {
setMemory(0);
}
function memoryRecall() {
setDisplay(formatNumber(memory));
setWaitingForOperand(true);
}
function memoryAdd() {
if (resetIfError()) {
return;
}
setMemory((currentMemory) => currentMemory + parseDisplay(display));
setWaitingForOperand(true);
}
function memorySubtract() {
if (resetIfError()) {
return;
}
setMemory((currentMemory) => currentMemory - parseDisplay(display));
setWaitingForOperand(true);
}
function memoryStore() {
if (resetIfError()) {
return;
}
setMemory(parseDisplay(display));
setWaitingForOperand(true);
}
function handleKeyboard(event: KeyboardEvent<HTMLDivElement>) {
const key = event.key;
const code = event.code;
let handled = true;
if (/^[0-9]$/.test(key)) {
inputDigit(key);
} else if (key === "," || key === "." || code === "NumpadDecimal") {
inputDecimal();
} else if (key === "+" || code === "NumpadAdd") {
chooseOperator("add");
} else if (key === "-" || code === "NumpadSubtract") {
chooseOperator("subtract");
} else if (key === "*" || code === "NumpadMultiply") {
chooseOperator("multiply");
} else if (key === "/" || code === "NumpadDivide") {
chooseOperator("divide");
} else if (key === "Enter" || key === "=" || code === "NumpadEnter") {
applyEquals();
} else if (key === "Backspace") {
backspace();
} else if (key === "Escape") {
clearAll();
} else if (key === "Delete") {
clearEntry();
} else if (key === "%") {
applyUnary("percent");
} else if (key === "F9") {
toggleSign();
} else {
handled = false;
}
if (handled) {
event.preventDefault();
event.stopPropagation();
}
}
return (
<div
className={`${styles.calculator} widgetNoDrag`}
tabIndex={0}
role="application"
aria-label="Taschenrechner"
onKeyDown={handleKeyboard}
>
<output className={styles.display} aria-label="Anzeige">
{display}
</output>
<div className={styles.memoryRow} aria-label="Speicherfunktionen">
<button type="button" className={styles.memoryButton} onClick={memoryClear} disabled={memory === 0}>
MC
</button>
<button type="button" className={styles.memoryButton} onClick={memoryRecall} disabled={memory === 0}>
MR
</button>
<button type="button" className={styles.memoryButton} onClick={memoryAdd}>
M+
</button>
<button type="button" className={styles.memoryButton} onClick={memorySubtract}>
M
</button>
<button type="button" className={styles.memoryButton} onClick={memoryStore}>
MS
</button>
<button type="button" className={styles.memoryButton} onClick={memoryRecall} disabled={memory === 0}>
M
</button>
</div>
<div className={styles.keypad}>
<CalculatorButton label="%" className={styles.utilityButton} onClick={() => applyUnary("percent")} />
<CalculatorButton label="CE" className={styles.utilityButton} onClick={clearEntry} />
<CalculatorButton label="C" className={styles.utilityButton} onClick={clearAll} />
<CalculatorButton label="⌫" ariaLabel="Rückschritt" className={styles.utilityButton} onClick={backspace} />
<CalculatorButton label="1/x" className={styles.utilityButton} onClick={() => applyUnary("reciprocal")} />
<CalculatorButton label="x²" className={styles.utilityButton} onClick={() => applyUnary("square")} />
<CalculatorButton label="²√x" className={styles.utilityButton} onClick={() => applyUnary("sqrt")} />
<CalculatorButton label="÷" className={styles.operatorButton} onClick={() => chooseOperator("divide")} />
<CalculatorButton label="7" className={styles.numberButton} onClick={() => inputDigit("7")} />
<CalculatorButton label="8" className={styles.numberButton} onClick={() => inputDigit("8")} />
<CalculatorButton label="9" className={styles.numberButton} onClick={() => inputDigit("9")} />
<CalculatorButton label="×" className={styles.operatorButton} onClick={() => chooseOperator("multiply")} />
<CalculatorButton label="4" className={styles.numberButton} onClick={() => inputDigit("4")} />
<CalculatorButton label="5" className={styles.numberButton} onClick={() => inputDigit("5")} />
<CalculatorButton label="6" className={styles.numberButton} onClick={() => inputDigit("6")} />
<CalculatorButton label="" className={styles.operatorButton} onClick={() => chooseOperator("subtract")} />
<CalculatorButton label="1" className={styles.numberButton} onClick={() => inputDigit("1")} />
<CalculatorButton label="2" className={styles.numberButton} onClick={() => inputDigit("2")} />
<CalculatorButton label="3" className={styles.numberButton} onClick={() => inputDigit("3")} />
<CalculatorButton label="+" className={styles.operatorButton} onClick={() => chooseOperator("add")} />
<CalculatorButton label="+/" className={styles.numberButton} onClick={toggleSign} />
<CalculatorButton label="0" className={styles.numberButton} onClick={() => inputDigit("0")} />
<CalculatorButton label="," className={styles.numberButton} onClick={inputDecimal} />
<CalculatorButton
label="="
ariaLabel={`Gleich ${getOperatorLabel(pendingOperator)}`}
className={styles.equalsButton}
onClick={applyEquals}
/>
</div>
</div>
);
}