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:
Claude
2026-06-18 10:02:05 +02:00
commit a4051ae132
74 changed files with 18317 additions and 0 deletions
+434
View File
@@ -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>
);
}