diff --git a/src/context/SettingsProvider.tsx b/src/context/SettingsProvider.tsx index 8952afb..5504bac 100644 --- a/src/context/SettingsProvider.tsx +++ b/src/context/SettingsProvider.tsx @@ -6,6 +6,8 @@ interface SettingsContextProps { setDarkMode: (v: boolean) => void; autoCheckin: boolean; setAutoCheckin: (v: boolean) => void; + hapticFeedback: boolean; + setHapticFeedback: (v: boolean) => void; soundEffect: string; setSoundEffect: (v: string) => void; } @@ -15,6 +17,8 @@ export const SettingsContext = createContext({ setDarkMode: () => {}, autoCheckin: false, setAutoCheckin: () => {}, + hapticFeedback: false, + setHapticFeedback: () => {}, soundEffect: 'None', setSoundEffect: () => {}, }); @@ -31,11 +35,23 @@ export const SettingsProvider = ({children}: {children: ReactNode}) => { const storedCheckin = JSON.parse(localStorage.getItem('autoCheckin') || 'false'); const [autoCheckin, setAutoCheckin] = useState(storedCheckin); + const storedHapticFeedback = JSON.parse(localStorage.getItem('hapticFeedback') || 'false'); + const [hapticFeedback, setHapticFeedback] = useState(storedHapticFeedback); + const [soundEffect, setSoundEffect] = useState(localStorage.getItem('soundEffect') || 'None'); return ( {children} diff --git a/src/pages/Events/checkin.ts b/src/pages/Events/checkin.ts index 4ae3460..4a1fb86 100644 --- a/src/pages/Events/checkin.ts +++ b/src/pages/Events/checkin.ts @@ -1,6 +1,7 @@ import {ErrorModalFunction} from '../../context/ModalContextProvider'; import db, {Event, Regform, Participant} from '../../db/db'; import {checkInParticipant} from '../../utils/client'; +import {playVibration} from '../../utils/haptics'; import {playSound} from '../../utils/sound'; import {handleError} from './sync'; @@ -23,6 +24,7 @@ export async function checkIn( participant: Participant, newCheckInState: boolean, sound: string, + hapticFeedback: boolean, errorModal: ErrorModalFunction ) { await db.participants.update(participant.id, {checkedInLoading: 1}); @@ -40,6 +42,9 @@ export async function checkIn( await updateCheckinState(regform, participant, newCheckInState); if (newCheckInState) { playSound(sound); + if (hapticFeedback) { + playVibration.success(); + } } } else { handleError(response, 'Something went wrong when updating check-in status', errorModal); diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index b19e12d..00d7f2a 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -27,8 +27,16 @@ export default function SettingsPage() { } function MainSettings() { - const {darkMode, setDarkMode, autoCheckin, setAutoCheckin, soundEffect, setSoundEffect} = - useSettings(); + const { + darkMode, + setDarkMode, + autoCheckin, + setAutoCheckin, + soundEffect, + setSoundEffect, + hapticFeedback, + setHapticFeedback, + } = useSettings(); const toggleDarkMode = () => { // Set the theme preference in localStorage and in the SettingsContext @@ -42,6 +50,11 @@ function MainSettings() { setAutoCheckin(!autoCheckin); }; + const toggleHapticFeedback = () => { + localStorage.setItem('hapticFeedback', (!hapticFeedback).toString()); + setHapticFeedback(!hapticFeedback); + }; + const onSoundEffectChange = (v: string) => { localStorage.setItem('soundEffect', v); setSoundEffect(v); @@ -63,6 +76,12 @@ function MainSettings() { selected={soundEffect} onChange={onSoundEffectChange} /> + diff --git a/src/pages/participant/ParticipantPage.tsx b/src/pages/participant/ParticipantPage.tsx index 7db3715..d9a3927 100644 --- a/src/pages/participant/ParticipantPage.tsx +++ b/src/pages/participant/ParticipantPage.tsx @@ -30,6 +30,7 @@ import {useErrorModal} from '../../hooks/useModal'; import useSettings from '../../hooks/useSettings'; import {useIsOffline} from '../../utils/client'; import {formatDatetime} from '../../utils/date'; +import {playVibration} from '../../utils/haptics'; import {playErrorSound} from '../../utils/sound'; import {checkIn} from '../Events/checkin'; import {syncEvent, syncParticipant, syncRegform} from '../Events/sync'; @@ -101,7 +102,7 @@ function ParticipantPageContent({ const navigate = useNavigate(); const {state} = useLocation(); const [autoCheckin, setAutoCheckin] = useState(state?.autoCheckin ?? false); - const {soundEffect} = useSettings(); + const {soundEffect, hapticFeedback} = useSettings(); const offline = useIsOffline(); const errorModal = useErrorModal(); const [notes, setNotes] = useState(participant?.notes || ''); @@ -120,13 +121,16 @@ function ParticipantPageContent({ showCheckedInWarning.current = false; if (participant?.checkedIn && participant?.checkedInDt) { playErrorSound(); + if (hapticFeedback) { + playVibration.error(); + } errorModal({ title: 'Participant already checked in', content: `This participant was checked in on ${formatDatetime(participant.checkedInDt)}`, }); } } - }, [participant, errorModal]); + }, [participant, errorModal, hapticFeedback]); const accompanyingPersons = useMemo(() => { if (participant?.registrationData) { @@ -143,13 +147,21 @@ function ParticipantPageContent({ } try { - await checkIn(event, regform, participant, newCheckinState, soundEffect, errorModal); + await checkIn( + event, + regform, + participant, + newCheckinState, + soundEffect, + hapticFeedback, + errorModal + ); } catch (err: any) { errorModal({title: 'Could not update check-in status', content: err.message}); } finally { } }, - [offline, errorModal, soundEffect] + [offline, errorModal, soundEffect, hapticFeedback] ); useEffect(() => { diff --git a/src/utils/haptics.ts b/src/utils/haptics.ts new file mode 100644 index 0000000..51527fe --- /dev/null +++ b/src/utils/haptics.ts @@ -0,0 +1,20 @@ +const patterns = { + error: [100, 50, 100], + success: [250], + clear: [], +}; + +function vibrate(pattern: number[]) { + if (!('vibrate' in navigator) || !navigator.userActivation.isActive) { + console.warn('Haptics not supported!'); + return; + } + + navigator.vibrate(pattern); +} + +export const playVibration = { + error: () => vibrate(patterns.error), + success: () => vibrate(patterns.success), + clear: () => vibrate(patterns.clear), // clear the vibration pattern (if needed) +};