From cb5a273725b4fb015e99d709542b4cf970af5936 Mon Sep 17 00:00:00 2001 From: sjschlapbach Date: Wed, 1 Nov 2023 14:21:45 +0100 Subject: [PATCH 1/3] refactor: rename components for new cycle countdown component setup --- src/Countdown.stories.tsx | 98 +++++++++++++++++----------------- src/Countdown.tsx | 97 +++++++++++---------------------- src/CountdownNew.stories.tsx | 80 --------------------------- src/CountdownNew.tsx | 50 ----------------- src/CycleCountdown.stories.tsx | 78 +++++++++++++++++++++++++++ src/CycleCountdown.tsx | 85 +++++++++++++++++++++++++++++ src/index.ts | 2 +- 7 files changed, 245 insertions(+), 245 deletions(-) delete mode 100644 src/CountdownNew.stories.tsx delete mode 100644 src/CountdownNew.tsx create mode 100644 src/CycleCountdown.stories.tsx create mode 100644 src/CycleCountdown.tsx diff --git a/src/Countdown.stories.tsx b/src/Countdown.stories.tsx index 87fd903..dd330e3 100644 --- a/src/Countdown.stories.tsx +++ b/src/Countdown.stories.tsx @@ -1,78 +1,80 @@ -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import Countdown from './Countdown' export const Default = () => { - return + let time = new Date() + time.setSeconds(time.getSeconds() + 40) + + return } export const Static = () => { - return ( - { - return { shouldRepeat: true, delay: 1.5 } - }} - /> - ) + let time = new Date() + time.setSeconds(time.getSeconds() + 40) + + return } -export const Repeating = () => { +export const Formatter = () => { + let time = new Date() + time.setSeconds(time.getSeconds() + 40) + return ( { - return { shouldRepeat: true, delay: 1.5 } + expiresAt={time} + formatter={(value) => { + return `${value} seconds` }} /> ) } -export const Coloring = () => { +export const onExpire = () => { + let time = new Date() + time.setSeconds(time.getSeconds() + 10) + return ( { - return { shouldRepeat: true, delay: 1.5 } + alert('expired') }} /> ) } -export const Formatted = () => { - return ( - - time === 0 ? ( -
Time's up
- ) : ( -
- {time}s
- remaining -
- ) - } - /> - ) -} +export const UpdateFunction = () => { + const time = useMemo(() => { + let t = new Date() + t.setSeconds(t.getSeconds() + 40) + return t + }, []) + + const [number, setNumber] = useState(0) -export const Expiration = () => { - const [expired, setExpired] = useState(false) return ( - <> +
{ - setExpired(true) + expiresAt={time} + onUpdate={(value) => { + setNumber(value) }} /> - Function executed: {expired ? 'yes' : 'no'} - + Number through onUpdate: {number} +
+ ) +} + +export const Styled = () => { + let time = new Date() + time.setSeconds(time.getSeconds() + 40) + + return ( + ) } diff --git a/src/Countdown.tsx b/src/Countdown.tsx index 2a8114b..837a4d9 100644 --- a/src/Countdown.tsx +++ b/src/Countdown.tsx @@ -1,84 +1,49 @@ import React from 'react' -import { CountdownCircleTimer } from 'react-countdown-circle-timer' +import ReactCountdown from 'react-countdown' -export interface CountdownProps { +interface CountdownProps { + isStatic?: boolean + expiresAt: Date + formatter?: (value: any) => any + onExpire?: () => void + onUpdate?: (timeRemaining: number) => void data?: { cy?: string test?: string } - countdownDuration: number - size?: number - strokeWidth?: number - colors?: [`#${string}`, `#${string}`, ...`#${string}`[]] - colorTimes?: [number, number, ...number[]] className?: { root?: string } - formatter?: (value: any) => any - onExpire?: () => void - onUpdate?: (timeRemaining: number) => void - isStatic?: boolean } -/** - * This function returnes a pre-styled Countdown component based on the react-countdown-circle-timer component. - * - * @param data - The object of data attributes that can be used for testing (e.g. data-test or data-cy) - * @param countdownDuration - The duration of the countdown in seconds. - * @param size - The size of the countdown in pixels. - * @param strokeWidth - The width of the countdown stroke in pixels. - * @param colors - The colors that are shown in the countdown (from the start to the end). The length of this array needs to be consistent with the colorTimes array. - * @param colorTimes - The times at which the colors change (automatic interpolation). The length of this array needs to be consistent with the colors array. - * @param className - The optional className object allows you to override the default styling. - * @param formatter - The function that is called to format the countdown value. - * @param onExpire - The function that is called when the countdown expires. - * @param onUpdate - The function that is called when the remaining time is updated. - * @param isStatic - Indicate whether the countdown is static (does not run) or not. - * @returns Countdown component - */ -export function Countdown({ - data, - countdownDuration, - colors, - colorTimes, - size, - strokeWidth, - className, +function Countdown({ + isStatic, + expiresAt, formatter, onExpire, onUpdate, - isStatic = false, -}: CountdownProps): React.ReactElement { + data, + className, +}: CountdownProps) { return ( -
- 0} - duration={countdownDuration > 0 ? countdownDuration : 0} - colors={colors || ['#00A321', '#00A321', '#F7B801', '#A30000']} - colorsTime={ - colorTimes || [ - countdownDuration, - (countdownDuration / 2) >> 0, - (countdownDuration / 4) >> 0, - 0, - ] - } - size={size || 45} - strokeWidth={strokeWidth || 7} - onComplete={onExpire} - onUpdate={onUpdate} - > - {({ remainingTime }: any) => { - return formatter - ? formatter(remainingTime) - : remainingTime > 0 - ? remainingTime - : 0 - }} - -
+ ( +
+ {formatter + ? formatter(Math.round(props.total / 1000)) + : Math.round(props.total / 1000)} +
+ )} + onComplete={onExpire} + onTick={(timeDelta) => onUpdate?.(timeDelta.total / 1000)} + /> ) } diff --git a/src/CountdownNew.stories.tsx b/src/CountdownNew.stories.tsx deleted file mode 100644 index 2a0cc2a..0000000 --- a/src/CountdownNew.stories.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useMemo, useState } from 'react' -import CountdownNew from './CountdownNew' - -export const Default = () => { - let time = new Date() - time.setSeconds(time.getSeconds() + 40) - - return -} - -export const Static = () => { - let time = new Date() - time.setSeconds(time.getSeconds() + 40) - - return -} - -export const Formatter = () => { - let time = new Date() - time.setSeconds(time.getSeconds() + 40) - - return ( - { - return `${value} seconds` - }} - /> - ) -} - -export const onExpire = () => { - let time = new Date() - time.setSeconds(time.getSeconds() + 10) - - return ( - { - alert('expired') - }} - /> - ) -} - -export const UpdateFunction = () => { - const time = useMemo(() => { - let t = new Date() - t.setSeconds(t.getSeconds() + 40) - return t - }, []) - - const [number, setNumber] = useState(0) - - return ( -
- { - setNumber(value) - }} - /> - Number through onUpdate: {number} -
- ) -} - -export const Styled = () => { - let time = new Date() - time.setSeconds(time.getSeconds() + 40) - - return ( - - ) -} diff --git a/src/CountdownNew.tsx b/src/CountdownNew.tsx deleted file mode 100644 index 18ab605..0000000 --- a/src/CountdownNew.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import Countdown from 'react-countdown' - -interface CountdownTextProps { - isStatic?: boolean - expiresAt: Date - formatter?: (value: any) => any - onExpire?: () => void - onUpdate?: (timeRemaining: number) => void - data?: { - cy?: string - test?: string - } - className?: { - root?: string - } -} - -function CountdownText({ - isStatic, - expiresAt, - formatter, - onExpire, - onUpdate, - data, - className, -}: CountdownTextProps) { - return ( - ( -
- {formatter - ? formatter(Math.round(props.total / 1000)) - : Math.round(props.total / 1000)} -
- )} - onComplete={onExpire} - onTick={(timeDelta) => onUpdate?.(timeDelta.total / 1000)} - /> - ) -} - -export default CountdownText diff --git a/src/CycleCountdown.stories.tsx b/src/CycleCountdown.stories.tsx new file mode 100644 index 0000000..b740d42 --- /dev/null +++ b/src/CycleCountdown.stories.tsx @@ -0,0 +1,78 @@ +import React, { useState } from 'react' +import Countdown from './CycleCountdown' + +export const Default = () => { + return +} + +export const Static = () => { + return ( + { + return { shouldRepeat: true, delay: 1.5 } + }} + /> + ) +} + +export const Repeating = () => { + return ( + { + return { shouldRepeat: true, delay: 1.5 } + }} + /> + ) +} + +export const Coloring = () => { + return ( + { + return { shouldRepeat: true, delay: 1.5 } + }} + /> + ) +} + +export const Formatted = () => { + return ( + + time === 0 ? ( +
Time's up
+ ) : ( +
+ {time}s
+ remaining +
+ ) + } + /> + ) +} + +export const Expiration = () => { + const [expired, setExpired] = useState(false) + return ( + <> + { + setExpired(true) + }} + /> + Function executed: {expired ? 'yes' : 'no'} + + ) +} diff --git a/src/CycleCountdown.tsx b/src/CycleCountdown.tsx new file mode 100644 index 0000000..2a8114b --- /dev/null +++ b/src/CycleCountdown.tsx @@ -0,0 +1,85 @@ +import React from 'react' +import { CountdownCircleTimer } from 'react-countdown-circle-timer' + +export interface CountdownProps { + data?: { + cy?: string + test?: string + } + countdownDuration: number + size?: number + strokeWidth?: number + colors?: [`#${string}`, `#${string}`, ...`#${string}`[]] + colorTimes?: [number, number, ...number[]] + className?: { + root?: string + } + formatter?: (value: any) => any + onExpire?: () => void + onUpdate?: (timeRemaining: number) => void + isStatic?: boolean +} + +/** + * This function returnes a pre-styled Countdown component based on the react-countdown-circle-timer component. + * + * @param data - The object of data attributes that can be used for testing (e.g. data-test or data-cy) + * @param countdownDuration - The duration of the countdown in seconds. + * @param size - The size of the countdown in pixels. + * @param strokeWidth - The width of the countdown stroke in pixels. + * @param colors - The colors that are shown in the countdown (from the start to the end). The length of this array needs to be consistent with the colorTimes array. + * @param colorTimes - The times at which the colors change (automatic interpolation). The length of this array needs to be consistent with the colors array. + * @param className - The optional className object allows you to override the default styling. + * @param formatter - The function that is called to format the countdown value. + * @param onExpire - The function that is called when the countdown expires. + * @param onUpdate - The function that is called when the remaining time is updated. + * @param isStatic - Indicate whether the countdown is static (does not run) or not. + * @returns Countdown component + */ +export function Countdown({ + data, + countdownDuration, + colors, + colorTimes, + size, + strokeWidth, + className, + formatter, + onExpire, + onUpdate, + isStatic = false, +}: CountdownProps): React.ReactElement { + return ( +
+ 0} + duration={countdownDuration > 0 ? countdownDuration : 0} + colors={colors || ['#00A321', '#00A321', '#F7B801', '#A30000']} + colorsTime={ + colorTimes || [ + countdownDuration, + (countdownDuration / 2) >> 0, + (countdownDuration / 4) >> 0, + 0, + ] + } + size={size || 45} + strokeWidth={strokeWidth || 7} + onComplete={onExpire} + onUpdate={onUpdate} + > + {({ remainingTime }: any) => { + return formatter + ? formatter(remainingTime) + : remainingTime > 0 + ? remainingTime + : 0 + }} + +
+ ) +} + +export default Countdown diff --git a/src/index.ts b/src/index.ts index 61691af..a810d33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ export * from './Checkbox' export * from './Collapsible' export * from './ColorPicker' export * from './Countdown' -export * from './CountdownNew' +export * from './CycleCountdown' export * from './CycleProgress' export * from './DateChanger' export * from './Dropdown' From 7d08027e9649182c671b184fe864fae965d4b832 Mon Sep 17 00:00:00 2001 From: sjschlapbach Date: Wed, 1 Nov 2023 16:08:35 +0100 Subject: [PATCH 2/3] feat(CycleCountdown): add cycle countdown component --- src/CycleCountdown.stories.tsx | 131 ++++++++++++++++++++------------- src/CycleCountdown.tsx | 123 ++++++++++++++----------------- 2 files changed, 136 insertions(+), 118 deletions(-) diff --git a/src/CycleCountdown.stories.tsx b/src/CycleCountdown.stories.tsx index b740d42..b2f034c 100644 --- a/src/CycleCountdown.stories.tsx +++ b/src/CycleCountdown.stories.tsx @@ -1,78 +1,107 @@ -import React, { useState } from 'react' -import Countdown from './CycleCountdown' +import React, { useMemo, useState } from 'react' +import CycleCountdown from './CycleCountdown' export const Default = () => { - return + let time = new Date() + time.setSeconds(time.getSeconds() + 10) + + return } export const Static = () => { - return ( - { - return { shouldRepeat: true, delay: 1.5 } - }} - /> - ) -} + let time = new Date() + time.setSeconds(time.getSeconds() + 10) -export const Repeating = () => { - return ( - { - return { shouldRepeat: true, delay: 1.5 } - }} - /> - ) + return } -export const Coloring = () => { +export const Small = () => { + let time = new Date() + time.setSeconds(time.getSeconds() + 10) + return ( - { - return { shouldRepeat: true, delay: 1.5 } - }} + ) } -export const Formatted = () => { +export const Formatter = () => { + let time = new Date() + time.setSeconds(time.getSeconds() + 10) + return ( - - time === 0 ? ( -
Time's up
- ) : ( -
- {time}s
- remaining -
- ) - } + { + return `${value} s` + }} /> ) } export const Expiration = () => { const [expired, setExpired] = useState(false) + let time = new Date() + time.setSeconds(time.getSeconds() + 10) + return ( - <> - + { setExpired(true) }} /> - Function executed: {expired ? 'yes' : 'no'} - + onExpire function executed: {expired ? 'yes' : 'no'} + + ) +} + +export const Updating = () => { + const time = useMemo(() => { + let t = new Date() + t.setSeconds(t.getSeconds() + 10) + return t + }, []) + + const [number, setNumber] = useState(0) + + return ( +
+ This countdown uses the onUpdate function to update a state value. Note + that we need to use a useMemo hook for the memoization of the time + variable, otherwise the countdown will be re-rendered every second. Also, + the time will not be updated on expiration and will therefore stay at 1 in + the end. + { + setNumber(value) + }} + /> + Updated value: {number} +
+ ) +} + +export const Styled = () => { + let time = new Date() + time.setSeconds(time.getSeconds() + 10) + + return ( + ) } diff --git a/src/CycleCountdown.tsx b/src/CycleCountdown.tsx index 2a8114b..cffc6a9 100644 --- a/src/CycleCountdown.tsx +++ b/src/CycleCountdown.tsx @@ -1,85 +1,74 @@ -import React from 'react' -import { CountdownCircleTimer } from 'react-countdown-circle-timer' +import dayjs from 'dayjs' +import React, { useState } from 'react' +import Countdown from './Countdown' +import CycleProgress from './CycleProgress' -export interface CountdownProps { +export interface CycleCountdownProps { + expiresAt: Date + totalDuration: number + size?: 'sm' | 'md' + color?: string + strokeWidthRem?: number + isStatic?: boolean + formatter?: (value: any) => any + onExpire?: () => void + onUpdate?: (timeRemaining: number) => void data?: { cy?: string test?: string } - countdownDuration: number - size?: number - strokeWidth?: number - colors?: [`#${string}`, `#${string}`, ...`#${string}`[]] - colorTimes?: [number, number, ...number[]] className?: { root?: string + countdownWrapper?: string + countdown?: string } - formatter?: (value: any) => any - onExpire?: () => void - onUpdate?: (timeRemaining: number) => void - isStatic?: boolean } -/** - * This function returnes a pre-styled Countdown component based on the react-countdown-circle-timer component. - * - * @param data - The object of data attributes that can be used for testing (e.g. data-test or data-cy) - * @param countdownDuration - The duration of the countdown in seconds. - * @param size - The size of the countdown in pixels. - * @param strokeWidth - The width of the countdown stroke in pixels. - * @param colors - The colors that are shown in the countdown (from the start to the end). The length of this array needs to be consistent with the colorTimes array. - * @param colorTimes - The times at which the colors change (automatic interpolation). The length of this array needs to be consistent with the colors array. - * @param className - The optional className object allows you to override the default styling. - * @param formatter - The function that is called to format the countdown value. - * @param onExpire - The function that is called when the countdown expires. - * @param onUpdate - The function that is called when the remaining time is updated. - * @param isStatic - Indicate whether the countdown is static (does not run) or not. - * @returns Countdown component - */ -export function Countdown({ - data, - countdownDuration, - colors, - colorTimes, - size, - strokeWidth, - className, +export function CycleCountdown({ + expiresAt, + totalDuration, + size = 'md', + color = '#00A321', + strokeWidthRem = 0.35, + isStatic, formatter, onExpire, onUpdate, - isStatic = false, -}: CountdownProps): React.ReactElement { + data, + className, +}: CycleCountdownProps) { + const [percentage, setPercentage] = useState( + (dayjs(expiresAt).diff(dayjs(), 'second') / totalDuration) * 100 + ) + return ( -
- 0} - duration={countdownDuration > 0 ? countdownDuration : 0} - colors={colors || ['#00A321', '#00A321', '#F7B801', '#A30000']} - colorsTime={ - colorTimes || [ - countdownDuration, - (countdownDuration / 2) >> 0, - (countdownDuration / 4) >> 0, - 0, - ] - } - size={size || 45} - strokeWidth={strokeWidth || 7} - onComplete={onExpire} - onUpdate={onUpdate} - > - {({ remainingTime }: any) => { - return formatter - ? formatter(remainingTime) - : remainingTime > 0 - ? remainingTime - : 0 + + { + onExpire?.() + setPercentage(0) + }} + onUpdate={(remainingSeconds) => { + onUpdate?.(remainingSeconds) + setPercentage((remainingSeconds / totalDuration) * 100) }} - -
+ className={{ root: className?.countdown }} + /> + ) } -export default Countdown +export default CycleCountdown From ce838dbd4983120b113a2137259c5634e3fb90b7 Mon Sep 17 00:00:00 2001 From: sjschlapbach Date: Wed, 1 Nov 2023 16:14:07 +0100 Subject: [PATCH 3/3] chore(CycleCountdown): add prop descriptions --- src/CycleCountdown.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/CycleCountdown.tsx b/src/CycleCountdown.tsx index cffc6a9..e3fcd98 100644 --- a/src/CycleCountdown.tsx +++ b/src/CycleCountdown.tsx @@ -24,6 +24,22 @@ export interface CycleCountdownProps { } } +/** + * This function combines the CycleProgress and Countdown components to create a circular progress bar with a countdown in the middle + * + * @param expiresAt - Date when the countdown should expire + * @param totalDuration - Total duration of the countdown in seconds, which is needed to compute the progress in percent + * @param size - Size of the progress bar, can be 'sm' or 'md' + * @param color - Color of the progress bar (static for the moment) + * @param strokeWidthRem - Width of the progress bar. For small size, a smaller value is recommended + * @param isStatic - If true, the countdown will not be running, but instead show the initial value. However, as the end value is given by a date, reloading can modify the displayed countdown value + * @param formatter - Function to format the countdown value + * @param onExpire - Function that is executed when the countdown expires + * @param onUpdate - Function that is executed when the countdown is updated (not when it expires) + * @param data - Optional data object that can be used for testing (e.g. data-test or data-cy) + * @param className - Optional className object allows you to override the default styling + * @returns A circular progress bar with a countdown in the middle + */ export function CycleCountdown({ expiresAt, totalDuration,