Skip to content

Commit

Permalink
feat(v3): play sound effects
Browse files Browse the repository at this point in the history
  • Loading branch information
jkrumm committed Jan 28, 2024
1 parent 623a5ee commit 41a69dd
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 61 deletions.
Binary file added public/sounds/join.wav
Binary file not shown.
Binary file added public/sounds/leave.wav
Binary file not shown.
Binary file added public/sounds/success.wav
Binary file not shown.
Binary file added public/sounds/tick.wav
Binary file not shown.
34 changes: 18 additions & 16 deletions src/components/room/counter.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useEffect, useState } from 'react';

import { Tooltip } from '@mantine/core';

import {
type MotionValue,
motion,
Expand Down Expand Up @@ -45,22 +43,23 @@ function Counter() {

return (
<div className="mb-[19px] border-2 border-amber-300">
<Tooltip label="Duration in minutes and seconds" color="#2E2E2E">
<div
style={{ fontSize }}
className="flex min-w-[90px] max-w-[90px] overflow-hidden justify-items-start text-white cursor-default"
>
<Digit place={10} value={minutes} />
<Digit place={1} value={minutes} />
<Digit place={10} value={seconds} />
<Digit place={1} value={seconds} />
</div>
</Tooltip>
<div
style={{ fontSize }}
className="flex min-w-[90px] max-w-[90px] overflow-hidden justify-items-start text-white cursor-default"
>
<DigitComponent place={10} value={minutes} />
<DigitComponent place={1} value={minutes} />
<DigitComponent place={10} value={seconds} />
<DigitComponent place={1} value={seconds} />
</div>
</div>
);
}

function Digit({ place, value }: { place: 1 | 10; value: number }) {
function DigitComponent({
place,
value,
}: Readonly<{ place: 1 | 10; value: number }>) {
const valueRoundedToPlace = Math.floor(value / place);
const animatedValue = useSpring(valueRoundedToPlace);

Expand All @@ -79,13 +78,16 @@ function Digit({ place, value }: { place: 1 | 10; value: number }) {
className={`relative w-[1ch] tabular-nums border border-[#fff] text-[#C1C2C5] bg-[#1F1F1F] px-3 ${additionalClasses}`}
>
{[...Array(10).keys()].map((i) => (
<Number key={i} mv={animatedValue} number={i} />
<NumberComponent key={i} mv={animatedValue} number={i} />
))}
</div>
);
}

function Number({ mv, number }: { mv: MotionValue; number: number }) {
function NumberComponent({
mv,
number,
}: Readonly<{ mv: MotionValue; number: number }>) {
const y = useTransform(mv, (latest) => {
const placeValue = latest % 10;
const offset = (10 + number - placeValue) % 10;
Expand Down
46 changes: 42 additions & 4 deletions src/components/room/interactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { useRouter } from 'next/router';
import { Button, Switch } from '@mantine/core';
import { notifications } from '@mantine/notifications';

import {
IconBell,
IconBellOff,
IconVolume,
IconVolumeOff,
} from '@tabler/icons-react';
import { type Logger } from 'next-axiom';

import { fibonacciSequence } from 'fpp/constants/fibonacci.constant';
Expand Down Expand Up @@ -35,6 +41,15 @@ export const Interactions = ({
}) => {
const router = useRouter();

// User localstorage state
const isPlaySound = useLocalstorageStore((store) => store.isPlaySound);
const setIsPlaySound = useLocalstorageStore((store) => store.setIsPlaySound);
const isNotificationsEnabled = useLocalstorageStore(
(store) => store.isNotificationsEnabled,
);
const setIsNotificationsEnabled = useLocalstorageStore(
(store) => store.setIsNotificationsEnabled,
);
const setRoomId = useLocalstorageStore((store) => store.setRoomId);
const setRoomReadable = useLocalstorageStore((store) => store.setRoomName);

Expand Down Expand Up @@ -104,12 +119,35 @@ export const Interactions = ({
: roomName.toUpperCase()}
</h2>
</Button>
<div>
<Button.Group>
<Button
variant={'default'}
onClick={() => {
setIsPlaySound(!isPlaySound);
}}
>
{isPlaySound ? (
<IconVolume size={20} />
) : (
<IconVolumeOff size={20} />
)}
</Button>
<Button
variant={'default'}
onClick={() => {
setIsNotificationsEnabled(!isNotificationsEnabled);
}}
>
{isNotificationsEnabled ? (
<IconBell size={20} />
) : (
<IconBellOff size={20} />
)}
</Button>
<Button
variant={
status === roomStateStatus.flipped ? 'filled' : 'default'
}
className={'mr-5'}
onClick={() => {
resetMutation.mutate({
roomId,
Expand Down Expand Up @@ -137,7 +175,7 @@ export const Interactions = ({
>
Leave
</Button>
</div>
</Button.Group>
</div>
<div className="voting-bar">
<Button.Group className="w-full">
Expand Down Expand Up @@ -179,7 +217,7 @@ export const Interactions = ({
}}
/>
<Switch
label="Auto Flip"
label="Auto Show"
className="cursor-pointer"
checked={isAutoFlip}
onChange={(event) => {
Expand Down
74 changes: 61 additions & 13 deletions src/server/room-state/room-state.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { TRPCError } from '@trpc/server';

import { notifications } from '@mantine/notifications';

import { getFromLocalstorage } from 'fpp/store/local-storage.store';

import { type ICreateVote } from 'fpp/server/db/schema';

import {
Expand Down Expand Up @@ -104,6 +106,35 @@ export function getStackedEstimationsFromUsers(
return voting.slice(0, getAverageFromUsers(users) > 9 ? 3 : 4);
}

function playSound(sound: 'join' | 'leave' | 'success' | 'tick') {
if (getFromLocalstorage('isPlaySound') === 'false') return;
const audio = new Audio(`/sounds/${sound}.wav`);
audio.volume = sound === 'success' || sound === 'tick' ? 0.4 : 0.3;
audio
.play()
.then(() => ({}))
.catch(() => ({}));
}

function notify({
color,
title,
message,
}: {
color: 'red' | 'orange' | 'blue';
title: string;
message: string;
}) {
if (getFromLocalstorage('isNotificationsEnabled') === 'false') return;
notifications.show({
color,
autoClose: 5000,
withCloseButton: true,
title,
message,
});
}

export function notifyOnRoomStateChanges({
newRoomState,
oldRoomState,
Expand All @@ -113,56 +144,73 @@ export function notifyOnRoomStateChanges({
newRoomState: {
users: User[];
isAutoFlip: boolean;
isFlipped: boolean;
};
oldRoomState: {
users: User[];
isAutoFlip: boolean;
isFlipped: boolean;
};
userId: string | null;
connectedAt: number | null;
}) {
// Make tick sound if an estimation or isSpectator changed
for (const newUser of newRoomState.users) {
const oldUser = oldRoomState.users.find((user) => user.id === newUser.id);
if (
oldUser &&
(oldUser.estimation !== newUser.estimation ||
oldUser.isSpectator !== newUser.isSpectator)
) {
playSound('tick');
}
}

// Make success sound if flipped
if (!oldRoomState.isFlipped && newRoomState.isFlipped) {
playSound('success');
}

// Notify on auto flip enabled
if (newRoomState.isAutoFlip && !oldRoomState.isAutoFlip) {
notifications.show({
notify({
color: 'orange',
autoClose: 5000,
withCloseButton: true,
title: 'Auto Flip enabled',
message: 'Voting will flip automatically once everyone estimated',
title: 'Auto flip enabled',
message: 'The cards will be flipped automatically once everyone voted',
});
return;
}

// Early return if user is connected in the last 5 seconds to prevent spam on entry
const recentlyConnected =
connectedAt === null || connectedAt > Date.now() - 1000 * 5;
if (recentlyConnected) {
return;
}

// Notify once new user joins and who it is
// Notify and join sound once new user joins and who it is
const newUser = newRoomState.users.find(
(user) => !oldRoomState.users.some((oldUser) => oldUser.id === user.id),
);

if (newUser && newUser.id !== userId) {
notifications.show({
playSound('join');
notify({
color: 'blue',
autoClose: 5000,
withCloseButton: true,
title: `${newUser.name} joined`,
message: 'User joined the room',
});
return;
}

// Notify once user leaves and who it is
// Notify and leave sound once user leaves and who it is
const leftUser = oldRoomState.users.find(
(user) => !newRoomState.users.some((newUser) => newUser.id === user.id),
);
if (leftUser) {
notifications.show({
playSound('leave');
notify({
color: 'red',
autoClose: 5000,
withCloseButton: true,
title: `${leftUser.name} left`,
message: 'User left the room',
});
Expand Down
42 changes: 25 additions & 17 deletions src/store/local-storage.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function saveToLocalstorage(key: string, value: string) {
localStorage.setItem(key, value);
}

function getFromLocalstorage(key: string): string | null {
export function getFromLocalstorage(key: string): string | null {
if (typeof window == 'undefined') {
return null;
}
Expand All @@ -31,28 +31,33 @@ function getIntFromLocalstorage(key: string): number | null {

interface LocalstorageStore {
username: string | null;
voting: number | null;
isPlaySound: boolean;
isNotificationsEnabled: boolean;
isSpectator: boolean;
roomId: number | null;
roomName: string | null;
recentRoom: string | null;
roomEvent: keyof typeof RoomEvent;
userId: string | null;
setUsername: (username: string) => void;
setVoting: (voting: number | null) => void;
setIsSpectator: (spectator: boolean) => void;
setRoomId: (room: number | null) => void;
setRoomName: (room: string | null) => void;
setRecentRoom: (room: string | null) => void;
setIsPlaySound: (isPlaySound: boolean) => void;
setIsNotificationsEnabled: (isNotificationsEnabled: boolean) => void;
setIsSpectator: (isSpectator: boolean) => void;
setRoomId: (roomId: number | null) => void;
setRoomName: (roomName: string | null) => void;
setRecentRoom: (recentRoom: string | null) => void;
setRoomEvent: (roomEvent: keyof typeof RoomEvent) => void;
setUserId: (userId: string) => void;
}

export const useLocalstorageStore = create<LocalstorageStore>((set, get) => ({
username: getFromLocalstorage('username'),
voting: getFromLocalstorage('vote')
? Number(getFromLocalstorage('vote'))
: null,
isPlaySound: getFromLocalstorage('isPlaySound')
? getFromLocalstorage('isPlaySound') === 'true'
: true,
isNotificationsEnabled: getFromLocalstorage('isNotificationsEnabled')
? getFromLocalstorage('isNotificationsEnabled') === 'true'
: true,
isSpectator: getFromLocalstorage('isSpectator') === 'true',
roomId: getIntFromLocalstorage('roomId'),
roomName: getFromLocalstorage('roomName'),
Expand Down Expand Up @@ -83,13 +88,16 @@ export const useLocalstorageStore = create<LocalstorageStore>((set, get) => ({
saveToLocalstorage('username', username);
set({ username });
},
setVoting: (voting: number | null) => {
if (voting === null) {
localStorage.removeItem('vote');
} else {
localStorage.setItem('vote', voting.toString());
}
set({ voting });
setIsPlaySound: (isPlaySound: boolean) => {
localStorage.setItem('isPlaySound', isPlaySound.toString());
set({ isPlaySound });
},
setIsNotificationsEnabled: (isNotificationsEnabled: boolean) => {
localStorage.setItem(
'isNotificationsEnabled',
isNotificationsEnabled.toString(),
);
set({ isNotificationsEnabled });
},
setIsSpectator: (isSpectator: boolean) => {
localStorage.setItem('isSpectator', isSpectator.toString());
Expand Down
24 changes: 13 additions & 11 deletions src/store/room-state.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,29 +49,31 @@ export const useRoomStateStore = create<RoomStateStore>((set, get) => ({
setConnectedAt: () => set({ connectedAt: Date.now() }),
// Interactions
update: (roomState: RoomStateClient) => {
const user = roomState.getUser(get().userId);
set({
// User
estimation: user.estimation,
isSpectator: user.isSpectator,
// Game State
users: roomState.users,
startedAt: roomState.startedAt,
isAutoFlip: roomState.isAutoFlip,
status: roomState.status,
});
notifyOnRoomStateChanges({
newRoomState: {
users: roomState.users,
isAutoFlip: roomState.isAutoFlip,
isFlipped: roomState.isFlipped,
},
oldRoomState: {
users: get().users,
isAutoFlip: get().isAutoFlip,
isFlipped: get().isFlipped,
},
userId: get().userId,
connectedAt: get().connectedAt,
});
const user = roomState.getUser(get().userId);
set({
// User
estimation: user.estimation,
isSpectator: user.isSpectator,
// Game State
users: roomState.users,
startedAt: roomState.startedAt,
isAutoFlip: roomState.isAutoFlip,
status: roomState.status,
});
},
reset: () => {
set({
Expand Down

1 comment on commit 41a69dd

@vercel
Copy link

@vercel vercel bot commented on 41a69dd Jan 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.