This repository has been archived by the owner on Jun 5, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Time): Add Time Component (#18)
* feat(Time): Add Time Component * Apply Prettier formatting for Time component, #18 * style(Time): lint time component * fix(Time): fix bad merge * refactor(Time): convert to Hook * docs(Time): update docs around ISO dates * spec(Time): add specs for Time component * style(Time): update style * refactor(Time): useRef instead of useState * refactor(Time): removed Yesterday check, made check for Day using LocalTime * docs: Readme update * chore: Move helper functions to separate files * Move dateTime prop note to propTypes definition This way docz shows the comment in its Props table component (...when it works) * feat: New useInterval React hook * refactor: Simplify Time component * chore: Fix typo * fix(useInterval): only set if delay is a valid number * fix: Accept undefined delay param in useInterval * style: merge conflict
- Loading branch information
1 parent
ba2c4e6
commit 56aedb7
Showing
7 changed files
with
293 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
--- | ||
name: Time | ||
menu: Components | ||
--- | ||
|
||
import {Playground, Props} from 'docz'; | ||
import Time from './'; | ||
|
||
# Time | ||
|
||
`Time` is a helper component for providing a reading-friendly time relative to a given context. | ||
|
||
For example, if something happened a few seconds ago, it would say "less than a minute ago". Similarly, if the provided timestamp was today, report the time as "3pm" rather than the full date. The idea is to provide not too much information. E.g., seconds aren't relevant when we're talking hours. | ||
|
||
The component auto-updates to keep the time accurate without requiring a page refresh. It renders a simple html `<time>` element with a `title` attribute that ensures that the full time is provided as a browser tooltip when holding a mouse cursor over it. | ||
|
||
## Props | ||
|
||
<Props of={Time} /> | ||
|
||
## Examples | ||
|
||
<Playground> | ||
<Time dateTime="2019-08-29T11:59:45Z" systemTime="2019-08-29T12:00:00Z" /> | ||
</Playground> | ||
|
||
<Playground> | ||
<Time dateTime="2019-08-29T11:57:00Z" systemTime="2019-08-29T12:00:00Z" /> | ||
</Playground> | ||
|
||
<Playground> | ||
<Time dateTime="2019-08-29T11:30:00Z" systemTime="2019-08-29T12:00:00Z" /> | ||
</Playground> | ||
|
||
<Playground> | ||
<Time dateTime="2019-08-28T11:30:00Z" systemTime="2019-08-29T12:00:00Z" /> | ||
</Playground> | ||
|
||
<Playground> | ||
<Time dateTime="2019-08-24T13:30:00Z" systemTime="2019-08-29T12:00:00Z" /> | ||
</Playground> | ||
|
||
<Playground> | ||
<Time dateTime="2019-01-24T13:30:00Z" systemTime="2019-08-29T12:00:00Z" /> | ||
</Playground> | ||
|
||
<Playground> | ||
<Time dateTime="1981-05-12" /> | ||
</Playground> | ||
|
||
## Invalid examples | ||
|
||
<Playground> | ||
<Time dateTime="3017-13-32T25:61:00" /> | ||
</Playground> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
const TIME_MINUTE = 60 * 1000; | ||
const TIME_HOUR = 60 * TIME_MINUTE; | ||
const TIME_DAY = 24 * TIME_HOUR; | ||
|
||
function date(d) { | ||
if (!d) { | ||
return null; | ||
} | ||
try { | ||
return new Date(d); | ||
} catch (e) { | ||
return null; | ||
} | ||
} | ||
|
||
function getDateString({dateTime, locale, systemOffset = 0}) { | ||
// Define the offset, how old is this... | ||
const time = date(dateTime); | ||
|
||
if (isNaN(time)) { | ||
return []; | ||
} | ||
|
||
// Update the system time | ||
const systemtime = new Date(); | ||
systemtime.setTime(Date.now() + systemOffset); | ||
const offset = time && systemtime && systemtime.getTime() - time.getTime(); | ||
|
||
// Get the number of milliseconds since midnight in the local tz | ||
const ms_today = | ||
(Date.now() - new Date().getTimezoneOffset() * 1000 * 60) % TIME_DAY; | ||
let dateString = 'n/a'; | ||
|
||
// Default delay | ||
let delay = null; | ||
|
||
// A few seconds ago | ||
if (offset < TIME_MINUTE / 2) { | ||
delay = TIME_MINUTE / 2 - offset; | ||
dateString = 'seconds ago'; | ||
} | ||
// Less than a minute ago | ||
else if (offset < TIME_MINUTE) { | ||
delay = TIME_MINUTE - offset; | ||
dateString = '< 1 minute ago'; | ||
} | ||
// A few minutes ago | ||
else if (offset < TIME_MINUTE * 10) { | ||
delay = TIME_MINUTE; | ||
const mins = parseInt(offset / TIME_MINUTE); | ||
dateString = `${mins} minute${mins > 1 ? 's' : ''} ago`; | ||
} | ||
// Occcured today... | ||
else if (offset < ms_today) { | ||
// Number of ms until end of the day... | ||
delay = TIME_DAY - ms_today; | ||
dateString = new Intl.DateTimeFormat(locale, { | ||
hour12: true, | ||
hour: 'numeric', | ||
minute: 'numeric', | ||
}).format(time); | ||
} | ||
// Occurred this week | ||
else if (offset < TIME_DAY * 6) { | ||
// Delay a day... | ||
delay = TIME_DAY; | ||
// Get day | ||
dateString = new Intl.DateTimeFormat(locale, { | ||
weekday: 'short', | ||
hour12: true, | ||
hour: 'numeric', | ||
}).format(time); | ||
} else if (time.getYear() === systemtime.getYear()) { | ||
dateString = new Intl.DateTimeFormat(locale, { | ||
month: 'short', | ||
day: 'numeric', | ||
}).format(time); | ||
} else { | ||
dateString = new Intl.DateTimeFormat(locale, { | ||
month: 'short', | ||
day: 'numeric', | ||
year: 'numeric', | ||
}).format(time); | ||
} | ||
return [dateString, delay]; | ||
} | ||
|
||
export {date, getDateString}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import React, {useReducer, useRef} from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import useInterval from '../useInterval'; | ||
import {date, getDateString} from './getDateString'; | ||
|
||
function useForceUpdate() { | ||
const [, forceUpdate] = useReducer(e => e + 1, 0); | ||
return forceUpdate; | ||
} | ||
|
||
function Time({dateTime, systemTime, locale}) { | ||
const forceUpdate = useForceUpdate(); | ||
|
||
// Offset system time with local time... | ||
const systemOffset = useRef( | ||
date(systemTime) ? date(systemTime).getTime() - Date.now() : 0 | ||
); | ||
|
||
// Get the date string and the delay before running the next loop | ||
const [dateString, delay] = getDateString({ | ||
dateTime, | ||
locale, | ||
systemOffset: systemOffset.current, | ||
}); | ||
|
||
// Set the datestring | ||
useInterval(forceUpdate, delay); | ||
|
||
const title = date(dateTime); | ||
|
||
return ( | ||
<time dateTime={dateTime} title={title && title.toLocaleString()}> | ||
{dateString || 'n/a'} | ||
</time> | ||
); | ||
} | ||
|
||
Time.propTypes = { | ||
/** Any Date string recognized by `Date.parse()`. | ||
* Suffix with `Z` to define the date as a UTC value or offset. */ | ||
dateTime: PropTypes.oneOfType([ | ||
PropTypes.instanceOf(Date), | ||
PropTypes.string, | ||
]).isRequired, | ||
systemTime: PropTypes.oneOfType([ | ||
PropTypes.instanceOf(Date), | ||
PropTypes.string, | ||
]), | ||
}; | ||
|
||
export default Time; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import React from 'react'; | ||
import {render, cleanup} from '@testing-library/react'; | ||
import Time from '.'; | ||
import 'jest-dom/extend-expect'; | ||
|
||
describe('Time', () => { | ||
afterEach(cleanup); | ||
|
||
[ | ||
{ | ||
offset: 1, | ||
text: 'seconds ago', | ||
}, | ||
{ | ||
offset: 33, | ||
text: '< 1 minute ago', | ||
}, | ||
{ | ||
offset: 90, | ||
text: '1 minute ago', | ||
}, | ||
].forEach(({offset, text}) => { | ||
it(`renders time as relative text, offset ${offset}, expect ${text}`, () => { | ||
const date = new Date(); | ||
date.setTime(date.getTime() - offset * 1000); | ||
|
||
const isoDate = date.toISOString(); | ||
|
||
const {container} = render(<Time dateTime={isoDate} />); | ||
|
||
const time = container.querySelector('time'); | ||
|
||
expect(time).toBeInTheDocument(); | ||
expect(time).toHaveAttribute('title', date.toLocaleString()); | ||
expect(time).toHaveAttribute('dateTime', isoDate); | ||
expect(time).toHaveTextContent(text); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
--- | ||
name: useInterval | ||
menu: Hooks | ||
--- | ||
|
||
import {Playground, Props} from 'docz'; | ||
|
||
# useInterval | ||
|
||
A hook for safely & declaratively setting up intervals in functional React components. | ||
|
||
Based on Dan Abramov's blog post ["Making setInterval Declarative with React Hooks"](https://overreacted.io/making-setinterval-declarative-with-react-hooks/) | ||
|
||
## Example | ||
|
||
```jsx | ||
import useInterval from 'base5-ui/useInterval'; | ||
|
||
function Counter() { | ||
const [count, setCount] = useState(0); | ||
|
||
useInterval(() => { | ||
// Your custom logic here | ||
setCount(count + 1); | ||
}, 1000); | ||
|
||
return <h1>{count}</h1>; | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import {useRef, useEffect} from 'react'; | ||
|
||
/** | ||
* @param {Function} callback - Function to run | ||
* @param {number|null|undefined} delay - Delay in milliseconds. Interval pauses when null | ||
*/ | ||
|
||
function useInterval(callback, delay) { | ||
const savedCallback = useRef(); | ||
|
||
// Remember the latest callback. | ||
useEffect(() => { | ||
savedCallback.current = callback; | ||
}); | ||
|
||
// Set up the interval. | ||
useEffect(() => { | ||
function tick() { | ||
savedCallback.current(); | ||
} | ||
if (typeof delay === 'number') { | ||
const id = setInterval(tick, delay); | ||
return () => clearInterval(id); | ||
} | ||
}, [delay]); | ||
} | ||
|
||
export default useInterval; |