Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: timer core #13

Open
wants to merge 15 commits into
base: v0
Choose a base branch
from
Open
76 changes: 76 additions & 0 deletions docs/framework/react/reference/useTimer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
title: Use Timer
id: useTimer
---

### useTimer

```ts
export function useTimer({
initialTime: number,
onFinished?: () => void,
timeZone?: Temporal.TimeZoneLike,
onStart?: () => void,
onStop?: () => void,
onReset?: () => void,
}): TimerApi;
```

`useTimer` is a hook that provides a comprehensive set of functionalities for managing a countdown timer, including starting, stopping, and resetting the timer. It also includes the ability to trigger a callback when the timer finishes, starts, stops, or resets.


#### Parameters

- `initialTime: number`
The initial time for the timer, specified in seconds.
- `timeZone?: Temporal.TimeZoneLike`
Optional time zone specification for the timer. Defaults to the system's time zone.
- `onFinished?: () => void`
An optional callback function that is called when the timer finishes.
- `onStart?: () => void`
Optional callback function that is called when the timer starts.
- `onStop?: () => void`
Optional callback function that is called when the timer stops.
- `onReset?: () => void`
Optional callback function that is called when the timer resets.


#### Returns

- `remainingTime: number`
This value represents the remaining time of the timer in seconds.
- `isRunning: boolean`
This value represents whether the timer is currently running.
- `start: () => void`
This function starts the timer.
- `stop: () => void`
This function stops the timer.
- `reset: () => void`
This function resets the timer to the initial time.


#### Example Usage

```ts
import { useTimer } from '@tanstack/react-time';

const TimerComponent = () => {
const { remainingTime, isRunning, start, stop, reset } = useTimer({
initialTime: 60,
onFinished: () => {
console.log('Timer finished!');
},
timeZone: 'America/New_York',
});

return (
<div>
<div>Remaining Time: {remainingTime}</div>
<div>Is Running: {isRunning ? 'Yes' : 'No'}</div>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
};
```
58 changes: 58 additions & 0 deletions docs/reference/timer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: Timer
id: timer
---

# Timer

```ts
export class Timer extends TimeCore<TimerState> implements TimerActions {
constructor(options: TimerOptions);
}
```

The Timer class provides functionality for managing a countdown timer, including starting, stopping, and resetting the timer. It also includes the ability to trigger a callback when the timer finishes.


## Parameters

- `initialTime: number`
The initial time for the timer, specified in seconds.
- `onFinished?: () => void`
An optional callback function that is called when the timer finishes.
- `timeZone?: Temporal.TimeZoneLike`
Optional time zone specification for the timer. Defaults to the system's time zone.
- `onStart?: () => void`
Optional callback function that is called when the timer starts.
- `onStop?: () => void`
Optional callback function that is called when the timer stops.
- `onReset?: () => void`
Optional callback function that is called when the timer resets.


## Methods

- `start(): void`
Starts the timer.
- `stop(): void`
Stops the timer.
- `reset(): void`
Resets the timer to the initial time.


## Example Usage

```ts
import { Timer } from '@tanstack/time';

const timer = new Timer({
initialTime: 60, // 60 seconds
onFinished: () => {
console.log('Timer finished!');
},
timeZone: 'America/New_York',
});

// Start the timer
timer.start();
```
1 change: 1 addition & 0 deletions packages/react-time/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"react-dom": "^17.0.0 || ^18.0.0"
},
"dependencies": {
"@tanstack/react-store": "^0.5.2",
"@tanstack/time": "workspace:*",
"use-sync-external-store": "^1.2.0"
},
Expand Down
99 changes: 99 additions & 0 deletions packages/react-time/src/tests/useTimer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { act, renderHook } from '@testing-library/react'
import { useTimer } from '../useTimer'

describe('useTimer', () => {
beforeEach(() => {
vi.useFakeTimers()
})

test('should start the timer', () => {
const { result } = renderHook(() => useTimer({ initialTime: 5 }))
act(() => {
result.current.start()
})
expect(result.current.isRunning).toBe(true)
})

test('should stop the timer', () => {
const { result } = renderHook(() => useTimer({ initialTime: 5 }))
act(() => {
result.current.start()
})
act(() => {
result.current.stop()
})
expect(result.current.isRunning).toBe(false)
})

test('should reset the timer', () => {
const { result } = renderHook(() => useTimer({ initialTime: 5 }))
act(() => {
result.current.start()
})
act(() => {
result.current.stop()
})
expect(result.current.isRunning).toBe(false)
expect(result.current.remainingTime).toBe(5)
})

test('should update the remaining time', () => {
const { result } = renderHook(() => useTimer({ initialTime: 5 }))
act(() => {
result.current.start()
})
act(() => {
vi.advanceTimersByTime(1000)
})
expect(result.current.remainingTime).toBe(4)
})

test('should call onStart callback', () => {
const onStart = vi.fn()
const { result } = renderHook(() => useTimer({ initialTime: 5, onStart }))
act(() => {
result.current.start()
})
expect(onStart).toHaveBeenCalledTimes(1)
})

test('should call onStop callback', () => {
const onStop = vi.fn()
const { result } = renderHook(() => useTimer({ initialTime: 5, onStop }))
act(() => {
result.current.start()
})
act(() => {
result.current.stop()
})
expect(onStop).toHaveBeenCalledTimes(1)
})

test('should call onReset callback', () => {
const onReset = vi.fn()
const { result } = renderHook(() => useTimer({ initialTime: 5, onReset }))
act(() => {
result.current.start()
})
act(() => {
result.current.stop()
})
act(() => {
result.current.reset()
})
expect(onReset).toHaveBeenCalledTimes(1)
})

test('should call onFinish callback', () => {
const onFinish = vi.fn()
const { result } = renderHook(() => useTimer({ initialTime: 5, onFinish }))
act(() => {
result.current.start()
})
act(() => {
vi.advanceTimersByTime(5000)
})
expect(onFinish).toHaveBeenCalledTimes(1)
})
})
22 changes: 22 additions & 0 deletions packages/react-time/src/useTimer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useStore } from '@tanstack/react-store'
import { Timer, type TimerApi, type TimerOptions } from '@tanstack/time'
import { useCallback, useState } from 'react'

export const useTimer = (options: TimerOptions): TimerApi => {
const [timer] = useState(() => new Timer(options))
const state = useStore(timer.store)

const start = useCallback<typeof timer.start>(() => {
timer.start()
}, [timer])

const stop = useCallback<typeof timer.stop>(() => {
timer.stop()
}, [timer])

const reset = useCallback<typeof timer.reset>(() => {
timer.reset()
}, [timer])

return { ...state, start, stop, reset }
}
9 changes: 8 additions & 1 deletion packages/time/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,12 @@
"files": [
"dist",
"src"
]
],
"dependencies": {
"@js-temporal/polyfill": "^0.4.4",
"@tanstack/store": "^0.4.1"
},
"devDependencies": {
"csstype": "^3.1.3"
}
}
1 change: 1 addition & 0 deletions packages/time/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './timer'
60 changes: 60 additions & 0 deletions packages/time/src/core/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Temporal } from '@js-temporal/polyfill'
import { Store } from '@tanstack/store'
import { getDefaultTimeZone } from '../utils/dateDefaults'

export interface TimeCoreOptions {
/**
* The time zone to use for the current time.
* @default Intl.DateTimeFormat().resolvedOptions().timeZone
*/
timeZone?: Temporal.TimeZoneLike
}

export interface TimeState {
/**
* The current time.
* @default Temporal.Now.zonedDateTimeISO()
* @readonly
* @type Temporal.ZonedDateTime
*/
currentTime: Temporal.ZonedDateTime
}

export abstract class TimeCore<TState extends TimeState> {
store: Store<TState>
interval: NodeJS.Timeout | null = null
timeZone: Temporal.TimeZoneLike

constructor(options: TimeCoreOptions = {}) {
const defaultTimeZone = getDefaultTimeZone()
this.timeZone = options.timeZone || defaultTimeZone
this.store = new Store<TState>({
currentTime: Temporal.Now.zonedDateTimeISO(this.timeZone),
} as TState)
this.updateCurrentTime()
}

protected updateCurrentTime() {
this.store.setState((prev) => ({
...prev,
currentTime: Temporal.Now.zonedDateTimeISO(this.timeZone),
}))
}

startUpdatingTime(intervalMs: number = 1000) {
if (!this.interval) {
this.interval = setInterval(() => this.updateCurrentTime(), intervalMs)
}
}

stopUpdatingTime() {
if (this.interval) {
clearInterval(this.interval)
this.interval = null
}
}

getCurrentTime(): Temporal.ZonedDateTime {
return this.store.state.currentTime
}
}
Loading