final notif changes

This commit is contained in:
2026-04-14 11:10:44 +05:30
parent a1ac8e7933
commit 19dcd73b29
11 changed files with 685 additions and 94 deletions

View File

@@ -0,0 +1,274 @@
import { useState, useRef, useCallback, useEffect } from 'react'
interface Props {
value: string // "HH:MM" 24-hour format
onChange: (value: string) => void
disabled?: boolean
}
const SIZE = 240
const CENTER = SIZE / 2
const CLOCK_RADIUS = 108
const NUM_RADIUS = 82
const HAND_RADIUS = 74
const TIP_RADIUS = 16
function polarToXY(angleDeg: number, radius: number) {
const rad = ((angleDeg - 90) * Math.PI) / 180
return {
x: CENTER + radius * Math.cos(rad),
y: CENTER + radius * Math.sin(rad),
}
}
function parseValue(v: string): { h: number; m: number } {
const [h, m] = v.split(':').map(Number)
return { h: isNaN(h) ? 8 : h, m: isNaN(m) ? 0 : m }
}
export default function ClockTimePicker({ value, onChange, disabled }: Props) {
const { h: initH, m: initM } = parseValue(value)
const [mode, setMode] = useState<'hours' | 'minutes'>('hours')
const [hour24, setHour24] = useState(initH)
const [minute, setMinute] = useState(initM)
const svgRef = useRef<SVGSVGElement>(null)
const isDragging = useRef(false)
// Keep mutable refs for use inside native event listeners
const modeRef = useRef(mode)
const isPMRef = useRef(initH >= 12)
const hour24Ref = useRef(initH)
const minuteRef = useRef(initM)
// Keep refs in sync with state
useEffect(() => { modeRef.current = mode }, [mode])
useEffect(() => { isPMRef.current = hour24 >= 12 }, [hour24])
useEffect(() => { hour24Ref.current = hour24 }, [hour24])
useEffect(() => { minuteRef.current = minute }, [minute])
// Sync when value prop changes externally
useEffect(() => {
const { h, m } = parseValue(value)
setHour24(h)
setMinute(m)
}, [value])
const isPM = hour24 >= 12
const hour12 = hour24 === 0 ? 12 : hour24 > 12 ? hour24 - 12 : hour24
const emit = useCallback(
(h24: number, m: number) => {
onChange(`${h24.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`)
},
[onChange]
)
const handleAmPm = (pm: boolean) => {
if (disabled) return
let newH = hour24
if (pm && hour24 < 12) newH = hour24 + 12
else if (!pm && hour24 >= 12) newH = hour24 - 12
setHour24(newH)
emit(newH, minute)
}
const applyAngle = useCallback(
(angle: number, currentMode: 'hours' | 'minutes') => {
if (currentMode === 'hours') {
const h12 = Math.round(angle / 30) % 12 || 12
const pm = isPMRef.current
const newH24 = pm ? (h12 === 12 ? 12 : h12 + 12) : (h12 === 12 ? 0 : h12)
setHour24(newH24)
emit(newH24, minuteRef.current)
} else {
const m = Math.round(angle / 6) % 60
setMinute(m)
emit(hour24Ref.current, m)
}
},
[emit]
)
const getSVGAngle = (clientX: number, clientY: number): number => {
if (!svgRef.current) return 0
const rect = svgRef.current.getBoundingClientRect()
const scale = rect.width / SIZE
const x = clientX - rect.left - CENTER * scale
const y = clientY - rect.top - CENTER * scale
return ((Math.atan2(y, x) * 180) / Math.PI + 90 + 360) % 360
}
// Mouse handlers (mouse events don't need passive:false)
const handleMouseDown = (e: React.MouseEvent<SVGSVGElement>) => {
if (disabled) return
isDragging.current = true
applyAngle(getSVGAngle(e.clientX, e.clientY), modeRef.current)
}
const handleMouseMove = (e: React.MouseEvent<SVGSVGElement>) => {
if (!isDragging.current || disabled) return
applyAngle(getSVGAngle(e.clientX, e.clientY), modeRef.current)
}
const handleMouseUp = (e: React.MouseEvent<SVGSVGElement>) => {
if (!isDragging.current) return
isDragging.current = false
applyAngle(getSVGAngle(e.clientX, e.clientY), modeRef.current)
if (modeRef.current === 'hours') setTimeout(() => setMode('minutes'), 120)
}
const handleMouseLeave = () => { isDragging.current = false }
// Attach non-passive touch listeners imperatively to avoid the passive warning
useEffect(() => {
const svg = svgRef.current
if (!svg) return
const onTouchStart = (e: TouchEvent) => {
if (disabled) return
e.preventDefault()
isDragging.current = true
const t = e.touches[0]
applyAngle(getSVGAngle(t.clientX, t.clientY), modeRef.current)
}
const onTouchMove = (e: TouchEvent) => {
if (!isDragging.current || disabled) return
e.preventDefault()
const t = e.touches[0]
applyAngle(getSVGAngle(t.clientX, t.clientY), modeRef.current)
}
const onTouchEnd = (e: TouchEvent) => {
if (!isDragging.current) return
e.preventDefault()
isDragging.current = false
const t = e.changedTouches[0]
applyAngle(getSVGAngle(t.clientX, t.clientY), modeRef.current)
if (modeRef.current === 'hours') setTimeout(() => setMode('minutes'), 120)
}
svg.addEventListener('touchstart', onTouchStart, { passive: false })
svg.addEventListener('touchmove', onTouchMove, { passive: false })
svg.addEventListener('touchend', onTouchEnd, { passive: false })
return () => {
svg.removeEventListener('touchstart', onTouchStart)
svg.removeEventListener('touchmove', onTouchMove)
svg.removeEventListener('touchend', onTouchEnd)
}
}, [applyAngle, disabled])
const handAngle = mode === 'hours' ? (hour12 / 12) * 360 : (minute / 60) * 360
const handTip = polarToXY(handAngle, HAND_RADIUS)
const displayH = hour12.toString()
const displayM = minute.toString().padStart(2, '0')
const selectedNum = mode === 'hours' ? hour12 : minute
const hourPositions = Array.from({ length: 12 }, (_, i) => {
const h = i + 1
return { h, ...polarToXY((h / 12) * 360, NUM_RADIUS) }
})
const minutePositions = Array.from({ length: 12 }, (_, i) => {
const m = i * 5
return { m, ...polarToXY((m / 60) * 360, NUM_RADIUS) }
})
return (
<div className="clock-picker">
{/* Time display */}
<div className="clock-picker__display">
<button
type="button"
className={`clock-picker__seg${mode === 'hours' ? ' clock-picker__seg--active' : ''}`}
onClick={() => !disabled && setMode('hours')}
>
{displayH}
</button>
<span className="clock-picker__colon">:</span>
<button
type="button"
className={`clock-picker__seg${mode === 'minutes' ? ' clock-picker__seg--active' : ''}`}
onClick={() => !disabled && setMode('minutes')}
>
{displayM}
</button>
<div className="clock-picker__ampm">
<button
type="button"
className={`clock-picker__ampm-btn${!isPM ? ' clock-picker__ampm-btn--active' : ''}`}
onClick={() => handleAmPm(false)}
disabled={disabled}
>AM</button>
<button
type="button"
className={`clock-picker__ampm-btn${isPM ? ' clock-picker__ampm-btn--active' : ''}`}
onClick={() => handleAmPm(true)}
disabled={disabled}
>PM</button>
</div>
</div>
{/* Clock face */}
<svg
ref={svgRef}
viewBox={`0 0 ${SIZE} ${SIZE}`}
className="clock-picker__face"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
style={{ cursor: disabled ? 'default' : 'pointer', touchAction: 'none', userSelect: 'none' }}
>
<circle cx={CENTER} cy={CENTER} r={CLOCK_RADIUS} className="clock-picker__bg" />
{/* Shaded sector */}
{(() => {
const start = polarToXY(0, HAND_RADIUS)
const end = polarToXY(handAngle, HAND_RADIUS)
const large = handAngle > 180 ? 1 : 0
return (
<path
d={`M ${CENTER} ${CENTER} L ${start.x} ${start.y} A ${HAND_RADIUS} ${HAND_RADIUS} 0 ${large} 1 ${end.x} ${end.y} Z`}
className="clock-picker__sector"
/>
)
})()}
<line x1={CENTER} y1={CENTER} x2={handTip.x} y2={handTip.y} className="clock-picker__hand" />
<circle cx={CENTER} cy={CENTER} r={4} className="clock-picker__center-dot" />
<circle cx={handTip.x} cy={handTip.y} r={TIP_RADIUS} className="clock-picker__hand-tip" />
{mode === 'hours' && hourPositions.map(({ h, x, y }) => (
<text key={h} x={x} y={y} textAnchor="middle" dominantBaseline="central"
className={`clock-picker__num${h === selectedNum ? ' clock-picker__num--selected' : ''}`}
>{h}</text>
))}
{mode === 'minutes' && minutePositions.map(({ m, x, y }) => (
<text key={m} x={x} y={y} textAnchor="middle" dominantBaseline="central"
className={`clock-picker__num${m === selectedNum ? ' clock-picker__num--selected' : ''}`}
>{m.toString().padStart(2, '0')}</text>
))}
{mode === 'minutes' && Array.from({ length: 60 }, (_, i) => {
if (i % 5 === 0) return null
const angle = (i / 60) * 360
const inner = polarToXY(angle, CLOCK_RADIUS - 10)
const outer = polarToXY(angle, CLOCK_RADIUS - 4)
return <line key={i} x1={inner.x} y1={inner.y} x2={outer.x} y2={outer.y} className="clock-picker__tick" />
})}
</svg>
{/* Mode pills */}
<div className="clock-picker__modes">
<button type="button"
className={`clock-picker__mode-btn${mode === 'hours' ? ' clock-picker__mode-btn--active' : ''}`}
onClick={() => !disabled && setMode('hours')}
>Hours</button>
<button type="button"
className={`clock-picker__mode-btn${mode === 'minutes' ? ' clock-picker__mode-btn--active' : ''}`}
onClick={() => !disabled && setMode('minutes')}
>Minutes</button>
</div>
</div>
)
}