Major rework: multi-user accounts (register/login, per-user data isolation), new layout with sidebar/dashboard/profile menu, matrix phase view with collapsible category columns, life timeline with ages per phase, live budget capping, collapsible transitions, mobile support
Deploy App / deploy (push) Successful in 1m47s

This commit is contained in:
2026-07-11 17:31:18 +02:00
parent 2e65bb3a2d
commit 685ff46c27
31 changed files with 1479 additions and 721 deletions
+17 -4
View File
@@ -54,14 +54,17 @@ export function NumberField({
// formatiert mit 1'000er-Trennzeichen an, solange das Feld nicht fokussiert ist, rundet
// beim Verlassen des Feldes auf ein Vielfaches von 1'000 ABwaerts (siehe lib/format.ts)
// und bietet Pfeil-Buttons zum Erhoehen/Verringern in 1'000er-Schritten.
// Optionales `max` kappt Eingaben live auf das verfuegbare Budget (z. B. Sparquote).
export function MoneyInput({
value,
onChange,
className,
max,
}: {
value: number;
onChange: (value: number) => void;
className?: string;
max?: number;
}) {
const [focused, setFocused] = useState(false);
const [text, setText] = useState(() => String(Math.floor(value || 0)));
@@ -72,8 +75,14 @@ export function MoneyInput({
const holdTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const holdInterval = useRef<ReturnType<typeof setInterval> | null>(null);
function clamp(v: number): number {
let result = Math.max(0, v);
if (max != null) result = Math.min(result, Math.max(0, floorToThousand(max)));
return result;
}
function step(delta: number) {
onChange(floorToThousand(valueRef.current) + delta);
onChange(clamp(floorToThousand(valueRef.current) + delta));
}
function stopHold() {
@@ -107,12 +116,14 @@ export function MoneyInput({
value={focused ? text : formatChf(value)}
onFocus={() => {
setFocused(true);
setText(String(Math.floor(value || 0)));
// Default-0 sofort leeren, damit man direkt lostippen kann.
const current = Math.floor(value || 0);
setText(current === 0 ? "" : String(current));
}}
onChange={(e) => setText(e.target.value.replace(/[^0-9-]/g, ""))}
onBlur={() => {
setFocused(false);
onChange(floorToThousand(parseChfInput(text)));
onChange(clamp(floorToThousand(parseChfInput(text))));
}}
/>
<div className="absolute inset-y-0 right-0 flex w-6 flex-col overflow-hidden rounded-r-lg border-l border-zinc-300 dark:border-zinc-700">
@@ -164,16 +175,18 @@ export function MoneyField({
help,
value,
onChange,
max,
}: {
label: string;
help?: string;
value: number;
onChange: (value: number) => void;
max?: number;
}) {
return (
<div>
<FieldLabel label={label} help={help} />
<MoneyInput value={value} onChange={onChange} />
<MoneyInput value={value} onChange={onChange} max={max} />
</div>
);
}