- Add viewport meta tag to prevent iOS zoom/scaling issues - Fix text color on iOS Safari (explicit text-gray-900 on buttons, inputs, TimePicker5) - Add session checks to /api/beos, /api/objekte, /api/wetter - Revert iframe embedding (X-Frame-Options: DENY, SameSite: lax) - docker-compose.prod.yml: fix DB_PORT=3306 for production - Add docker-compose.prod.yml, .env.prod.example, dump/import scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
'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 text-gray-900">{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>
|
|
);
|
|
}
|