Skip to content
This repository has been archived by the owner on Jun 5, 2023. It is now read-only.

Commit

Permalink
feat(Time): Add Time Component (#18)
Browse files Browse the repository at this point in the history
* 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
MrSwitch authored and diondiondion committed Sep 2, 2019
1 parent ba2c4e6 commit 56aedb7
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 0 deletions.
55 changes: 55 additions & 0 deletions src/Time/README.mdx
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>
88 changes: 88 additions & 0 deletions src/Time/getDateString.js
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};
52 changes: 52 additions & 0 deletions src/Time/index.js
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;
39 changes: 39 additions & 0 deletions src/Time/index.spec.js
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);
});
});
});
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export {default as Switch} from './Switch';
export {default as Text} from './Text';
export {default as TextLink} from './TextLink';
export {default as ThemeSection} from './ThemeSection';
export {default as Time} from './Time';
export {default as useBreakpoints} from './useBreakpoints';
export {default as useInterval} from './useInterval';
export {default as ViewMoreText} from './ViewMoreText';
export {default as VisuallyHidden} from './VisuallyHidden';
29 changes: 29 additions & 0 deletions src/useInterval/README.mdx
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>;
}
```
28 changes: 28 additions & 0 deletions src/useInterval/index.js
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;

0 comments on commit 56aedb7

Please sign in to comment.