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
Deploy App / deploy (push) Successful in 1m47s
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user