Various UX improvements and bug fixes
- Fix mustChangePassword session flag for users with pw=NULL - Add PrF (Private Führung) as new ArtFuehrung type - Split datetime-local into separate date + TimePicker5 (5-min steps, auto-repeat) - Responsive Beginn/Ende layout: stacked on mobile, inline on desktop - Sort BEOs alphabetically by Kürzel in selector - Title shows active kuppel; hide user display in header - Selected BEOs show Kürzel only (name stays in dropdown) - Session timeout reduced to 1 hour - Add CLAUDE.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
68
components/TimePicker5.tsx
Normal file
68
components/TimePicker5.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
|
||||
interface Props {
|
||||
value: string; // "HH:MM"
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function addMinutes(time: string, delta: number): string {
|
||||
const [h, m] = time.split(':').map(Number);
|
||||
const total = ((h * 60 + m + delta) % (24 * 60) + 24 * 60) % (24 * 60);
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${pad(Math.floor(total / 60))}:${pad(total % 60)}`;
|
||||
}
|
||||
|
||||
export default function TimePicker5({ value, onChange, className = '' }: Props) {
|
||||
const valueRef = useRef(value);
|
||||
valueRef.current = value;
|
||||
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
function startRepeat(delta: number) {
|
||||
onChange(addMinutes(valueRef.current, delta));
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
const hourDelta = delta > 0 ? 60 : -60;
|
||||
intervalRef.current = setInterval(() => {
|
||||
onChange(addMinutes(valueRef.current, hourDelta));
|
||||
}, 350);
|
||||
}, 400);
|
||||
}
|
||||
|
||||
function stopRepeat() {
|
||||
if (timeoutRef.current !== null) { clearTimeout(timeoutRef.current); timeoutRef.current = null; }
|
||||
if (intervalRef.current !== null) { clearInterval(intervalRef.current); intervalRef.current = null; }
|
||||
}
|
||||
|
||||
function buttonProps(delta: number) {
|
||||
return {
|
||||
type: 'button' as const,
|
||||
tabIndex: -1,
|
||||
onMouseDown: () => startRepeat(delta),
|
||||
onMouseUp: stopRepeat,
|
||||
onMouseLeave: stopRepeat,
|
||||
onTouchStart: (e: React.TouchEvent) => { e.preventDefault(); startRepeat(delta); },
|
||||
onTouchEnd: stopRepeat,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); onChange(addMinutes(value, 5)); }
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); onChange(addMinutes(value, -5)); }
|
||||
}}
|
||||
className={`flex items-center border-2 border-gray-400 rounded-lg bg-white focus:border-blue-500 focus:outline-none select-none ${className}`}
|
||||
>
|
||||
<span className="flex-1 px-3 py-2 text-sm font-mono text-center">{value}</span>
|
||||
<div className="flex flex-col border-l border-gray-300 shrink-0">
|
||||
<button {...buttonProps(5)} className="px-2 pt-1 pb-0.5 hover:bg-gray-100 text-gray-500 text-xs leading-none">▲</button>
|
||||
<button {...buttonProps(-5)} className="px-2 pt-0.5 pb-1 hover:bg-gray-100 text-gray-500 text-xs leading-none">▼</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user