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>
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user