Skip to content

Commit

Permalink
Add haptic feedback (#62)
Browse files Browse the repository at this point in the history
* Add haptic feedback

* Fix `navigator.vibrate` check, address review comments
  • Loading branch information
GovernmentPlates authored Jul 18, 2024
1 parent b82c470 commit 10cb387
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 7 deletions.
18 changes: 17 additions & 1 deletion src/context/SettingsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -15,6 +17,8 @@ export const SettingsContext = createContext<SettingsContextProps>({
setDarkMode: () => {},
autoCheckin: false,
setAutoCheckin: () => {},
hapticFeedback: false,
setHapticFeedback: () => {},
soundEffect: 'None',
setSoundEffect: () => {},
});
Expand All @@ -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 (
<SettingsContext.Provider
value={{darkMode, setDarkMode, autoCheckin, setAutoCheckin, soundEffect, setSoundEffect}}
value={{
darkMode,
setDarkMode,
autoCheckin,
setAutoCheckin,
soundEffect,
setSoundEffect,
hapticFeedback,
setHapticFeedback,
}}
>
{children}
</SettingsContext.Provider>
Expand Down
5 changes: 5 additions & 0 deletions src/pages/Events/checkin.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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});
Expand All @@ -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);
Expand Down
23 changes: 21 additions & 2 deletions src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -63,6 +76,12 @@ function MainSettings() {
selected={soundEffect}
onChange={onSoundEffectChange}
/>
<SettingsToggle
title="Haptic feedback"
description="Vibrate on certain interactions (e.g. check-in, error etc.)"
checked={hapticFeedback}
onToggle={toggleHapticFeedback}
/>
</SettingsSection>
<SettingsSection title="Appearance">
<SettingsToggle title="Dark theme" checked={darkMode} onToggle={toggleDarkMode} />
Expand Down
20 changes: 16 additions & 4 deletions src/pages/participant/ParticipantPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 || '');
Expand All @@ -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) {
Expand All @@ -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(() => {
Expand Down
20 changes: 20 additions & 0 deletions src/utils/haptics.ts
Original file line number Diff line number Diff line change
@@ -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)
};

0 comments on commit 10cb387

Please sign in to comment.