From 4bf1370543525f7ddde0a88451aee5498b299969 Mon Sep 17 00:00:00 2001 From: Louis Mazin Date: Fri, 25 Sep 2026 10:47:55 +0200 Subject: [PATCH] test --- frontend/src/components/ResponsivePickers.jsx | 537 ++++++++++++++++++ frontend/src/pages/CalendarPage.jsx | 49 +- frontend/src/pages/TimeClockPage.jsx | 448 +-------------- 3 files changed, 552 insertions(+), 482 deletions(-) create mode 100644 frontend/src/components/ResponsivePickers.jsx diff --git a/frontend/src/components/ResponsivePickers.jsx b/frontend/src/components/ResponsivePickers.jsx new file mode 100644 index 0000000..5206151 --- /dev/null +++ b/frontend/src/components/ResponsivePickers.jsx @@ -0,0 +1,537 @@ +import { useRef, useState } from 'react'; +import { Calendar } from '@/components/ui/calendar'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { CalendarDays, Clock } from 'lucide-react'; +import { fr } from 'date-fns/locale'; + +const parseDateValue = (value) => { + if (!value) return undefined; + + const [ + year, + month, + day, + ] = value.split('-').map(Number); + + if (!year || !month || !day) { + return undefined; + } + + return new Date( + year, + month - 1, + day, + ); +}; + + +const formatDateValue = (date) => { + if (!date) return ''; + + const year = + date.getFullYear(); + + const month = String( + date.getMonth() + 1, + ).padStart(2, '0'); + + const day = String( + date.getDate(), + ).padStart(2, '0'); + + return `${year}-${month}-${day}`; +}; + + +const formatDateLabel = (value) => { + const date = + parseDateValue(value); + + if (!date) { + return 'Choisir une date'; + } + + const numericDate = + date.toLocaleDateString( + 'fr-FR', + { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }, + ); + + const longDate = + date.toLocaleDateString( + 'fr-FR', + { + day: '2-digit', + month: 'long', + year: 'numeric', + }, + ); + + return ( + <> + + {longDate} + + + + {numericDate} + + + ); +}; + + +const DesktopDatePicker = ({ + value, + onChange, + min, + placeholder = 'Choisir une date', +}) => { + const [open, setOpen] = + useState(false); + + const selectedDate = + parseDateValue(value); + + const minDate = + parseDateValue(min); + + return ( + + + } + > + + + + {selectedDate + ? formatDateLabel(value) + : placeholder} + + + + + { + if (!date) return; + + onChange( + formatDateValue(date), + ); + + setOpen(false); + }} + /> + + + ); +}; + + +/* ========================================================= + TIME PICKER +========================================================= */ + +const DesktopTimePicker = ({ + value, + onChange +}) => { + const [open, setOpen] = useState(false); + const timeRef = useRef(value); + timeRef.current = value; + const mouseState = useRef({ active: false, lastY: 0, accumulated: 0 }); + + const touchState = useRef({ + active: false, + lastY: 0, + accumulated: 0, + }); + + const [hours = '', minutes = ''] = value + ? value.split(':') + : []; + + const hourOptions = Array.from( + { length: 24 }, + (_, i) => String(i).padStart(2, '0'), + ); + + const minuteOptions = Array.from( + { length: 60 }, + (_, i) => String(i).padStart(2, '0'), + ); + + const updateTime = ( + nextHours, + nextMinutes, + ) => { + const nextValue = `${String(nextHours).padStart(2, '0')}:${String( + nextMinutes, + ).padStart(2, '0')}`; + timeRef.current = nextValue; + onChange(nextValue); + }; + + const changeHour = (direction) => { + const [currentHour = 0, currentMinute = 0] = + (timeRef.current || '00:00').split(':').map(Number); + updateTime((currentHour + direction % 24 + 24) % 24, currentMinute); + }; + + const changeMinute = (direction) => { + const [currentHour = 0, currentMinute = 0] = + (timeRef.current || '00:00').split(':').map(Number); + const total = ((currentHour * 60 + currentMinute + direction) % 1440 + 1440) % 1440; + updateTime(Math.floor(total / 60), total % 60); + }; + + const handleMouseDown = (event) => { + if (event.pointerType !== 'mouse' || event.button !== 0) return; + event.preventDefault(); + event.currentTarget.setPointerCapture(event.pointerId); + mouseState.current = { active: true, lastY: event.clientY, accumulated: 0 }; + }; + + const handleMouseMove = (event, type) => { + if (!mouseState.current.active || event.pointerType !== 'mouse') return; + const state = mouseState.current; + state.accumulated += state.lastY - event.clientY; + state.lastY = event.clientY; + const steps = Math.trunc(state.accumulated / 18); + if (!steps) return; + state.accumulated -= steps * 18; + if (type === 'hours') changeHour(steps); + else changeMinute(steps); + }; + + const handleMouseUp = () => { + mouseState.current.active = false; + mouseState.current.accumulated = 0; + }; + + const handleWheel = (event, type) => { + event.stopPropagation(); + + const direction = + event.deltaY > 0 ? 1 : -1; + + if (type === 'hours') { + changeHour(direction); + } else { + changeMinute(direction); + } + }; + + const handleTouchStart = (event) => { + const touch = event.touches[0]; + + touchState.current = { + active: true, + lastY: touch.clientY, + accumulated: 0, + }; + }; + + const handleTouchMove = (event, type) => { + if (!touchState.current.active) { + return; + } + + const touch = event.touches[0]; + const currentY = touch.clientY; + + const deltaY = + touchState.current.lastY - currentY; + + touchState.current.lastY = currentY; + + touchState.current.accumulated += deltaY; + + const pixelsPerUnit = 18; + + const steps = Math.trunc( + touchState.current.accumulated / + pixelsPerUnit, + ); + + if (steps === 0) { + return; + } + + touchState.current.accumulated -= + steps * pixelsPerUnit; + + if (type === 'hours') { + changeHour(steps); + } else { + changeMinute(steps); + } + + event.stopPropagation(); + }; + + const handleTouchEnd = () => { + touchState.current.active = false; + touchState.current.accumulated = 0; + }; + + const clearTime = () => { + onChange(''); + setOpen(false); + }; + + return ( + + + } + > + + + + {value ? ( + value + ) : ( + <> + + Choisir une heure + + + + Choisir + + + )} + + + + +
+ + Choisir une heure + + + + {value || '--:--'} + +
+ +
+
+ +
handleMouseMove(event, 'hours')} + onPointerUp={handleMouseUp} + onLostPointerCapture={handleMouseUp} + onWheel={(event) => + handleWheel( + event, + 'hours', + ) + } + onTouchStart={handleTouchStart} + onTouchMove={(event) => + handleTouchMove( + event, + 'hours', + ) + } + onTouchEnd={handleTouchEnd} + onTouchCancel={handleTouchEnd} + > +
+ + { + hourOptions[ + (Number(hours || 0) + + 23) % + 24 + ] + } + + + + {hours || '00'} + + + + { + hourOptions[ + (Number(hours || 0) + + 1) % + 24 + ] + } + +
+
+ +
+ + : + +
+ +
handleMouseMove(event, 'minutes')} + onPointerUp={handleMouseUp} + onLostPointerCapture={handleMouseUp} + onWheel={(event) => + handleWheel( + event, + 'minutes', + ) + } + onTouchStart={handleTouchStart} + onTouchMove={(event) => + handleTouchMove( + event, + 'minutes', + ) + } + onTouchEnd={handleTouchEnd} + onTouchCancel={handleTouchEnd} + > +
+ + { + minuteOptions[ + (Number(minutes || 0) + + 59) % + 60 + ] + } + + + + {minutes || '00'} + + + + { + minuteOptions[ + (Number(minutes || 0) + + 1) % + 60 + ] + } + +
+
+
+ +

+ Glissez pour modifier +

+ +
+ +
+ + + ); +}; + + +// Les champs natifs ouvrent le sélecteur du téléphone. +// Sur ordinateur, on conserve le calendrier et la molette de TimeClock. +export const DatePicker = ({ value, onChange, min, placeholder, id }) => ( + <> + onChange(event.target.value)} + className="block w-full min-w-0 md:hidden" + /> +
+ +
+ +); + +export const TimePicker = ({ value, onChange, id }) => ( + <> + onChange(event.target.value)} + className="block w-full min-w-0 md:hidden" + /> +
+ +
+ +); diff --git a/frontend/src/pages/CalendarPage.jsx b/frontend/src/pages/CalendarPage.jsx index 431c40f..7fa8bff 100644 --- a/frontend/src/pages/CalendarPage.jsx +++ b/frontend/src/pages/CalendarPage.jsx @@ -17,6 +17,7 @@ import LoadingCard from '@/components/LoadingCard'; import MainCard from '@/components/MainCard'; import HoverInfo from '@/components/HoverInfo'; +import { DatePicker, TimePicker } from '@/components/ResponsivePickers'; import { @@ -2358,27 +2359,21 @@ const CalendarPage = () => { - + onChange={(value) => handleStartDateChange( - event.target.value, + value, ) @@ -2396,12 +2391,10 @@ const CalendarPage = () => { - { } - onChange={( - - event, - - ) => + onChange={(value) => handleFormChange( 'end_date', - event.target.value, + value, ) @@ -2456,29 +2445,23 @@ const CalendarPage = () => { - + onChange={(value) => handleFormChange( 'start_time', - event.target.value, + value, ) @@ -2502,29 +2485,23 @@ const CalendarPage = () => { - + onChange={(value) => handleFormChange( 'end_time', - event.target.value, + value, ) diff --git a/frontend/src/pages/TimeClockPage.jsx b/frontend/src/pages/TimeClockPage.jsx index 6244c2e..bdb53b2 100644 --- a/frontend/src/pages/TimeClockPage.jsx +++ b/frontend/src/pages/TimeClockPage.jsx @@ -31,6 +31,7 @@ import { import { Badge } from '@/components/ui/badge'; import ConfirmDialog from '@/components/ConfirmDialog'; +import { DatePicker, TimePicker } from '@/components/ResponsivePickers'; import { Dialog, @@ -39,16 +40,9 @@ import { DialogTitle, } from '@/components/ui/dialog'; -import { Calendar } from '@/components/ui/calendar'; + import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; - -import { - CalendarDays, Clock, } from 'lucide-react'; @@ -95,24 +89,6 @@ const parseDateValue = (value) => { }; -const formatDateValue = (date) => { - if (!date) return ''; - - const year = - date.getFullYear(); - - const month = String( - date.getMonth() + 1, - ).padStart(2, '0'); - - const day = String( - date.getDate(), - ).padStart(2, '0'); - - return `${year}-${month}-${day}`; -}; - - const formatDateLabel = (value) => { const date = parseDateValue(value); @@ -183,426 +159,6 @@ const formatDate = ( DATE PICKER ========================================================= */ -const DatePicker = ({ - value, - onChange, - min, - placeholder = 'Choisir une date', -}) => { - const [open, setOpen] = - useState(false); - - const selectedDate = - parseDateValue(value); - - const minDate = - parseDateValue(min); - - return ( - - - } - > - - - - {selectedDate - ? formatDateLabel(value) - : placeholder} - - - - - { - if (!date) return; - - onChange( - formatDateValue(date), - ); - - setOpen(false); - }} - /> - - - ); -}; - - -/* ========================================================= - TIME PICKER -========================================================= */ - -const TimePicker = ({ - value, - onChange -}) => { - const [open, setOpen] = useState(false); - - const touchState = useRef({ - active: false, - lastY: 0, - accumulated: 0, - }); - - const [hours = '', minutes = ''] = value - ? value.split(':') - : []; - - const hourOptions = Array.from( - { length: 24 }, - (_, i) => String(i).padStart(2, '0'), - ); - - const minuteOptions = Array.from( - { length: 60 }, - (_, i) => String(i).padStart(2, '0'), - ); - - const updateTime = ( - nextHours, - nextMinutes, - ) => { - onChange( - `${String(nextHours).padStart(2, '0')}:${String( - nextMinutes, - ).padStart(2, '0')}`, - ); - }; - - const changeHour = (direction) => { - const currentHour = Number(hours || 0); - const currentMinute = Number(minutes || 0); - - let nextHour = - currentHour + direction; - - if (nextHour < 0) { - nextHour = 23; - } - - if (nextHour > 23) { - nextHour = 0; - } - - updateTime( - nextHour, - currentMinute, - ); - }; - - const changeMinute = (direction) => { - const currentHour = Number(hours || 0); - const currentMinute = Number(minutes || 0); - - let nextMinute = - currentMinute + direction; - - let nextHour = currentHour; - - if (nextMinute < 0) { - nextMinute = 59; - - nextHour = - currentHour === 0 - ? 23 - : currentHour - 1; - } - - if (nextMinute > 59) { - nextMinute = 0; - - nextHour = - currentHour === 23 - ? 0 - : currentHour + 1; - } - - updateTime( - nextHour, - nextMinute, - ); - }; - - const handleWheel = (event, type) => { - event.stopPropagation(); - - const direction = - event.deltaY > 0 ? 1 : -1; - - if (type === 'hours') { - changeHour(direction); - } else { - changeMinute(direction); - } - }; - - const handleTouchStart = (event) => { - const touch = event.touches[0]; - - touchState.current = { - active: true, - lastY: touch.clientY, - accumulated: 0, - }; - }; - - const handleTouchMove = (event, type) => { - if (!touchState.current.active) { - return; - } - - const touch = event.touches[0]; - const currentY = touch.clientY; - - const deltaY = - touchState.current.lastY - currentY; - - touchState.current.lastY = currentY; - - touchState.current.accumulated += deltaY; - - const pixelsPerUnit = 18; - - const steps = Math.trunc( - touchState.current.accumulated / - pixelsPerUnit, - ); - - if (steps === 0) { - return; - } - - touchState.current.accumulated -= - steps * pixelsPerUnit; - - if (type === 'hours') { - changeHour(steps); - } else { - changeMinute(steps); - } - - event.stopPropagation(); - }; - - const handleTouchEnd = () => { - touchState.current.active = false; - touchState.current.accumulated = 0; - }; - - const clearTime = () => { - onChange(''); - setOpen(false); - }; - - return ( - - - } - > - - - - {value ? ( - value - ) : ( - <> - - Choisir une heure - - - - Choisir - - - )} - - - - -
- - Choisir une heure - - - - {value || '--:--'} - -
- -
-
- -
- handleWheel( - event, - 'hours', - ) - } - onTouchStart={handleTouchStart} - onTouchMove={(event) => - handleTouchMove( - event, - 'hours', - ) - } - onTouchEnd={handleTouchEnd} - onTouchCancel={handleTouchEnd} - > -
- - { - hourOptions[ - (Number(hours || 0) + - 23) % - 24 - ] - } - - - - {hours || '00'} - - - - { - hourOptions[ - (Number(hours || 0) + - 1) % - 24 - ] - } - -
-
- -
- - : - -
- -
- handleWheel( - event, - 'minutes', - ) - } - onTouchStart={handleTouchStart} - onTouchMove={(event) => - handleTouchMove( - event, - 'minutes', - ) - } - onTouchEnd={handleTouchEnd} - onTouchCancel={handleTouchEnd} - > -
- - { - minuteOptions[ - (Number(minutes || 0) + - 59) % - 60 - ] - } - - - - {minutes || '00'} - - - - { - minuteOptions[ - (Number(minutes || 0) + - 1) % - 60 - ] - } - -
-
-
- -

- Glissez pour modifier -

- -
- -
- - - ); -}; - - /* ========================================================= PAGE ========================================================= */