Skip to content

Commit

Permalink
feat: add timer to retro
Browse files Browse the repository at this point in the history
* feat: implemented custom timepicker component for the timer

* feat: added timer, timer button and dialog

* feat: add disabled prop to timepicker

* fix: implement PR feedback+fixed sec. display prob

* fix: custom duration, NaN check bug, PR feedback

* fix: improved number checking

* fix: timer display in picker when running

* fix: renamed method

* feat: added custom confetti when timer ends

* fix: rearranged particles packages

* fix: simplified timer + new timers start correct

* feat: timer can be paused and resumed

* feat: add color to timer pausing

* fix: removed increment/decrement buttons

* fix: button color change, timer edge case

* feat: add timer events in the retroContext

* feat: timer over retrocontext

* fix: time running end condition

* fix: remove decrement / increment time

* feat: add pause timer functionality

* fix: show remaining time when paused

* fix: add timer state values on import

* fix: renamed button & timer default value

* fix: limit input field to max length

* feat: error state disables timer start

* feat: button wiggles on finish

* fix: remove particle modules

* fix: timer button is visible while wiggling

* fix: cleanup

* fix: clean timepicker structure, removed ":"

* fix: cleanup

* fix: rework WiggleActionButton

* fix: renamed CreateTimerDialog

* fix: extract finish effect in hook

* fix: correct label at timer above 60 minutes

* feat: add timer increment buttons

* fix: prevent unnecessary effect timeouts

* fix: reshape button properties

* fix: extract incrementTimerButton

* fix: improve timelabel implementation

* feat: eslint checks line between functions

* fix: internalized label

* fix: remove indirection in useValidatedTimeInput

* fix: added line between const and function to eslint

* fix: restructured components & cleanup

* fix: rearranged closing, optimized naming + conditions

* feat: add eslint empty line between function & return

* fix: set new duration at PAUSE_TIMER

* fix: calculate milliseconds extracted to util

* fix: remove curly braces

* fix: add increment timer state

* refactor: rename increment timer action to change timer action

* fix: show timer finish animation for participants

* refactor: remove unnecessary setting the timer again when pausing the timer

---------

Co-authored-by: Manuel Lehé <manuel.lehe@maibornwolff.de>
Co-authored-by: Ben Willenbring <ben.willenbring@maibornwolff.de>
  • Loading branch information
3 people authored Dec 15, 2023
1 parent b4f420c commit c96652c
Show file tree
Hide file tree
Showing 25 changed files with 540 additions and 15 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,11 @@ module.exports = {
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/triple-slash-reference": "off",
"prettier/prettier": "warn",
"padding-line-between-statements": [
"warn",
{ blankLine: "always", prev: "block-like", next: "function" },
{ blankLine: "always", prev: "const", next: "function" },
{ blankLine: "always", prev: "function", next: "return" },
],
},
};
2 changes: 1 addition & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"@mui/lab": "5.0.0-alpha.117",
"@mui/material": "^5.11.6",
"@shared/browserlogger": "*",
"@shared/socket": "*",
"@shared/configuration": "*",
"@shared/socket": "*",
"axios": "^1.6.0",
"lodash": "^4.17.21",
"nanoid": "^4.0.0",
Expand Down
11 changes: 5 additions & 6 deletions packages/frontend/src/common/components/buttons/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import { Button, ButtonProps, useTheme } from "@mui/material";
import React from "react";
import React, { MouseEventHandler } from "react";

interface ActionButtonProps extends ButtonProps {
onClick: () => void;
onClick: MouseEventHandler<HTMLButtonElement>;
label: string;
isDisabled?: boolean;
icon?: React.ReactNode;
}

export function ActionButton({ onClick, isDisabled, label, icon, ...props }: ActionButtonProps) {
export function ActionButton({ onClick, label, icon, ...props }: ActionButtonProps) {
const theme = useTheme();

return (
<Button
{...props}
variant="contained"
onClick={onClick}
disabled={isDisabled}
sx={{
m: 1,
borderRadius: theme.spacing(2),
boxShadow: "0px 5px 10px 0px rgba(0, 0, 0, 0.5)",
whiteSpace: "nowrap",
width: theme.spacing(22),
...props.sx,
}}
startIcon={icon ?? undefined}
startIcon={icon}
fullWidth
>
{label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function PokerResultButton() {
<ActionButton
onClick={handleClick}
label="Show Results"
isDisabled={noUserVoted}
disabled={noUserVoted}
icon={<Visibility />}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function ResetVotesButton() {
<ActionButton
onClick={openDialog}
label="Reset Votes"
isDisabled={noUserVoted}
disabled={noUserVoted}
icon={<RestartAlt />}
/>
<ResetVotesDialog isOpen={isOpen} close={closeDialog} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function CreatePokerSessionDialog() {
redirectToRoom(roomId);
handleClose();
}

return (
<Dialog
fullWidth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CallToActionButton } from "../../../common/components/buttons/CallToAct
export function ResetVotesDialog({ isOpen, close }: DialogProps) {
const { handleResetUserStory } = usePokerContext();
const fullScreen = useFullscreen();

function handleClick() {
handleResetUserStory();
close();
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/src/retro/components/RetroActionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { ToggleRetroBlurButton } from "./buttons/ToggleRetroBlurButton";
import { CreateColumnButton } from "./buttons/CreateColumnButton";
import { FlexBox } from "../../common/components/FlexBox";
import { ToggleRetroVotingButton } from "./buttons/ToggleRetroVotingButton";
import { ToggleTimerDialogButton } from "./buttons/ToggleTimerDialogButton";

export function RetroActionButtons() {
return (
<FlexBox flexDirection="row" p={1}>
<CreateColumnButton />
<ToggleRetroBlurButton />
<ToggleRetroVotingButton />
<ToggleTimerDialogButton />
</FlexBox>
);
}
59 changes: 59 additions & 0 deletions packages/frontend/src/retro/components/TimePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from "react";
import { FlexBox } from "../../common/components/FlexBox";
import { TextInput } from "../../common/components/TextInput";
import IncrementTimerButton from "./buttons/IncrementTimerButton";
interface TimePickerProps {
minutes: string;
seconds: string;
disabled: boolean;
isMinutesError: boolean;
isSecondsError: boolean;
onSecondsChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onMinutesChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onSubmit: () => void;
onTimerIncrement: (increment: number) => void;
}
export function TimePicker({
minutes,
seconds,
disabled,
isMinutesError,
isSecondsError,
onSecondsChange,
onMinutesChange,
onSubmit,
onTimerIncrement,
}: TimePickerProps) {
return (
<>
<FlexBox gap={2}>
<TextInput
value={minutes}
onChange={onMinutesChange}
onSubmit={onSubmit}
autoFocus
required
disabled={disabled}
type="tel"
error={isMinutesError}
label="Minutes"
/>
<TextInput
value={seconds}
onChange={onSecondsChange}
onSubmit={onSubmit}
required
type="tel"
disabled={disabled}
error={isSecondsError}
label="Seconds"
/>
</FlexBox>
<FlexBox>
<IncrementTimerButton onTimerIncrement={onTimerIncrement} minutesToIncrement={1} />
<IncrementTimerButton onTimerIncrement={onTimerIncrement} minutesToIncrement={3} />
<IncrementTimerButton onTimerIncrement={onTimerIncrement} minutesToIncrement={5} />
</FlexBox>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function CreateColumnButton() {
<ActionButton
onClick={openDialog}
label={"Add Column"}
isDisabled={!isModerator(user)}
disabled={!isModerator(user)}
icon={<Add />}
/>
<CreateColumnDialog isOpen={isOpen} close={closeDialog} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Publish } from "@mui/icons-material";
import { ListItemIcon, ListItemText, MenuItem } from "@mui/material";
import { useUserContext } from "../../../common/context/UserContext";
import { RetroSchemaV1 } from "../../types/retroSchema";
import { RetroState } from "../../types/retroTypes";
import { RetroState, TimerStatus } from "../../types/retroTypes";
import { useRetroContext } from "../../context/RetroContext";
import { isModerator } from "../../../common/utils/participantsUtils";

Expand All @@ -29,6 +29,8 @@ export function ImportRetroMenuItem() {
participants: {},
waitingList: {},
isVotingEnabled: false,
timerStatus: TimerStatus.STOPPED,
timerDuration: 0,
};
handleSetRetroState(retro);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";
import { ActionButton } from "../../../common/components/buttons/ActionButton";

interface IncrementTimerButtonProps {
onTimerIncrement: (amount: number) => void;
minutesToIncrement: number;
}
export default function IncrementTimerButton({
onTimerIncrement,
minutesToIncrement,
}: IncrementTimerButtonProps) {
return (
<ActionButton
label={`+${minutesToIncrement} Minutes`}
onClick={() => {
onTimerIncrement(minutesToIncrement);
}}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from "react";
import { Alarm, SnoozeOutlined } from "@mui/icons-material";
import { isModerator } from "../../../common/utils/participantsUtils";
import { TimerDialog } from "../dialogs/TimerDialog";
import { useRetroContext } from "../../context/RetroContext";
import { useUserContext } from "../../../common/context/UserContext";
import { useDialog } from "../../../common/hooks/useDialog";
import { useTimer } from "../../hooks/useTimer";

import { TimerStatus } from "../../types/retroTypes";
import { WiggleActionButton } from "./WiggleActionButton";
import useTimedEffect from "../../hooks/useTimedEffect";

export function ToggleTimerDialogButton() {
const { isOpen, closeDialog, openDialog } = useDialog();
const { retroState } = useRetroContext();
const { timerStatus } = retroState;
const { user } = useUserContext();

const { isEffectActive, startEffect } = useTimedEffect({ effectLength: 3000 });
const { minutes, seconds, remainingTimeLabel } = useTimer({ onTimerFinish: startEffect });

function handleOpenDialog() {
if (!isModerator(user)) return;
openDialog();
}

if (!isModerator(user) && timerStatus === TimerStatus.STOPPED && !isEffectActive) return null;

return (
<>
<WiggleActionButton
onClick={handleOpenDialog}
label={timerStatus !== TimerStatus.STOPPED ? remainingTimeLabel : "Timer"}
icon={timerStatus === TimerStatus.PAUSED ? <SnoozeOutlined /> : <Alarm />}
color={
timerStatus === TimerStatus.PAUSED
? "info"
: timerStatus === TimerStatus.RUNNING || isEffectActive
? "error"
: undefined
}
isWiggling={isEffectActive}
/>
<TimerDialog
isOpen={isOpen}
close={closeDialog}
remainingMinutes={minutes}
remainingSeconds={seconds}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { MouseEventHandler } from "react";
import { ButtonProps } from "@mui/material";
import { keyframes } from "@emotion/react";
import { ActionButton } from "../../../common/components/buttons/ActionButton";

interface WiggleActionButtonProps extends ButtonProps {
onClick: MouseEventHandler<HTMLButtonElement>;
label: string;
icon?: React.ReactNode;
isWiggling: boolean;
}

export function WiggleActionButton({
onClick,
label,
icon,
isWiggling,
...props
}: WiggleActionButtonProps) {
const wiggle = keyframes`
0%, 20%, 40%, 60%, 80%, 100% {
transform: rotate(0deg);
}
5%, 25%, 45%, 65%, 85% {
transform: rotate(5deg);
}
10%, 30%, 50%, 70%, 90% {
transform: rotate(-5deg);
}
15%, 35%, 55%, 75%, 95% {
transform: rotate(0deg);
}
`;
return (
<ActionButton
label={label}
onClick={onClick}
icon={icon}
sx={{ animation: isWiggling ? `${wiggle} 0.5s ease infinite` : undefined }}
{...props}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface SortColumnMenuItemProps {
export const SortColumnMenuItem = React.forwardRef(
({ column }: SortColumnMenuItemProps, ref: any) => {
const { handleSortCardsByVotesDescending } = useRetroContext();

function sortByVotesDescending() {
handleSortCardsByVotesDescending(column.index);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function QrCodeDialog({ isOpen, close }: DialogProps) {
async function onRendered() {
await QRCode.toCanvas(qrCanvas.current, window.location.href);
}

return (
<Dialog
fullScreen={fullScreen}
Expand Down
Loading

0 comments on commit c96652c

Please sign in to comment.