├── .github
└── workflows
│ ├── ci.yml
│ └── demo.yml
├── .gitignore
├── LICENSE
├── README.md
├── commitlint.config.js
├── package-lock.json
├── package.json
└── packages
├── demo
├── __tests__
│ ├── App.test.tsx
│ └── setup.ts
├── eslint.config.js
├── index.html
├── package.json
├── set-react-version.js
├── src
│ ├── App.css
│ ├── App.tsx
│ ├── main-legacy.tsx
│ ├── main-new.tsx
│ └── vite-env.d.ts
├── test-react-versions.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vitest.config.ts
└── timeago-react
├── .gitignore
├── .prettierrc
├── __tests__
├── setup.ts
└── timeago-react.test.tsx
├── eslint.config.mjs
├── package.json
├── postbuild.js
├── src
└── timeago-react.tsx
├── tsconfig.ci.json
├── tsconfig.json
└── vitest.config.ts
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | ci:
7 | runs-on: ubuntu-latest
8 | timeout-minutes: 10
9 | strategy:
10 | matrix:
11 | node-version: [lts/*, current]
12 | env:
13 | CI: true
14 | steps:
15 | - name: Checkout ${{ github.sha }}
16 | uses: actions/checkout@v4
17 | - name: Use Node.js ${{ matrix.node-version }}
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: ${{ matrix.node-version }}
21 | registry-url: https://registry.npmjs.org/
22 | - name: Install dependencies
23 | run: npm ci
24 | - name: Lint
25 | run: npm run lint --if-present
26 | - name: Build
27 | run: npm run build --if-present
28 | - name: Test
29 | run: npm run test --if-present
30 | - name: Publish coverage
31 | if: matrix.node-version == 'current'
32 | uses: coverallsapp/github-action@v2
33 | with:
34 | format: clover
35 | file: packages/timeago-react/coverage/clover.xml
36 | github-token: ${{ secrets.GITHUB_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.github/workflows/demo.yml:
--------------------------------------------------------------------------------
1 | name: demo
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build-and-deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout ${{ github.sha }}
13 | uses: actions/checkout@v4
14 | - name: Use Node.js lts/*
15 | uses: actions/setup-node@v4
16 | with:
17 | node-version: lts/*
18 | registry-url: https://registry.npmjs.org/
19 | - name: Install dependencies
20 | run: npm ci
21 | - name: Build
22 | run: npm run build --if-present
23 | - name: Deploy
24 | uses: peaceiris/actions-gh-pages@v3
25 | with:
26 | github_token: ${{ secrets.GITHUB_TOKEN }}
27 | publish_dir: ./packages/demo/dist
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.*
3 | .idea
4 | coverage
5 | cjs/
6 | esm/
7 | dist/
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) hustcc
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # timeago-react
2 |
3 |
4 | > timeago-react is a simple react component used to format date with `*** time ago` statement. eg: '3 hours ago'.
5 |
6 | **The component based on [timeago.js](https://github.com/hustcc/timeago.js)** which is a simple javascript module.
7 |
8 | - Realtime render. Automatic release the resources.
9 | - Simple. Only 2kb.
10 | - Efficient. When the time is `3 hour ago`, the interval will an hour (3600 * 1000 ms).
11 | - Locales supported.
12 |
13 | [](https://www.npmjs.com/package/timeago-react)
14 | [](https://github.com/hustcc/timeago-react)
15 | [](https://github.com/hustcc/timeago-react)
16 | [](https://www.npmjs.com/package/timeago-react)
17 | [](https://github.com/hustcc/timeago-react)
18 | [](https://www.npmjs.com/package/timeago-react)
19 |
20 |
21 | ## Install
22 |
23 | > **npm install timeago-react**
24 |
25 |
26 | ## Usage
27 |
28 | ```jsx
29 | import * as React from 'react';
30 | import TimeAgo from 'timeago-react'; // var TimeAgo = require('timeago-react');
31 |
32 |
36 | ```
37 |
38 |
39 | ## Component props
40 |
41 | - **`datetime`** (required, string / Date / timestamp)
42 |
43 | The datetime to be formatted. can be `datetime string`, `Date instance`, or `timestamp`.
44 |
45 | - **`live`** (optional, boolean)
46 |
47 | Live render, default is `true`.
48 |
49 | - **`className`** (optional, string)
50 |
51 | The `class` of span. you can setting the css style of span by class name.
52 |
53 | - **`opts.relativeDate`** (optional, string / Date / timestamp)
54 |
55 | The datetime to be calculated interval relative to.
56 |
57 | - **`opts.minInterval`** (optional, number in seconds)
58 |
59 | The min interval in seconds to update the ** time ago string
60 |
61 | - **`locale`** (optional, string)
62 |
63 | The `locale` language of statement, default is `en`. All supported locales [here](https://github.com/hustcc/timeago.js/tree/master/src/lang). If you want to use locale which is not `zh_CN` / `en`, you should import the locale before use it. As below:
64 |
65 | ```jsx
66 | import * as React from 'react';
67 | import TimeAgo from 'timeago-react';
68 | import * as timeago from 'timeago.js';
69 |
70 | // import it first.
71 | import vi from 'timeago.js/lib/lang/vi';
72 |
73 | // register it.
74 | timeago.register('vi', vi);
75 |
76 | // then use it.
77 |
81 | ```
82 |
83 | - **`style`** (optional, object)
84 |
85 | The `style` object to applied to the root element.
86 |
87 | Props not documented above are applied to the root element.
88 |
89 |
90 | ## LICENSE
91 |
92 | MIT
93 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'type-empty': [2, 'never'],
4 | 'type-enum': [
5 | 2,
6 | 'always',
7 | ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test', 'wip'],
8 | ],
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "timeago-react-workspace",
3 | "private": true,
4 | "workspaces": [
5 | "packages/*"
6 | ],
7 | "scripts": {
8 | "start": "npm run start --workspace=demo",
9 | "lint": "npm run lint --workspaces",
10 | "build": "npm run build --workspace=timeago-react && npm run build --workspace=demo",
11 | "test": "npm run test --workspaces"
12 | },
13 | "devDependencies": {
14 | "husky": "^4.3.8"
15 | },
16 | "husky": {
17 | "hooks": {
18 | "pre-commit": "npm run lint-staged --workspace=timeago-react",
19 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/demo/__tests__/App.test.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
2 | import * as React from "react";
3 | import { render, screen } from "@testing-library/react";
4 | import { test, expect, beforeAll, vi } from "vitest";
5 | import App from "../src/App";
6 |
7 | // Mock new Date() to return a fixed date for consistent testing
8 | beforeAll(() => {
9 | const mockDate = new Date("2025-04-03T23:21:33.000Z");
10 | vi.setSystemTime(mockDate);
11 | });
12 |
13 | test("renders basic TimeAgo component", () => {
14 | render();
15 | screen.getByText("just now");
16 | });
17 |
18 | test("renders Vietnamese locale example", () => {
19 | render();
20 | screen.getByText(/\d+ năm trước/);
21 | });
22 |
23 | test("renders non-live TimeAgo example", () => {
24 | render();
25 | screen.getByText("11 seconds ago");
26 | });
27 |
28 | test("renders styled TimeAgo example", () => {
29 | render();
30 | const styledTimeElement = screen.getByText("7 years ago");
31 | expect(styledTimeElement.style.color).toBe("green");
32 | });
33 |
34 | test("renders relative date example", () => {
35 | render();
36 | screen.getByText("1 day ago");
37 | });
38 |
39 | test("renders TimeAgo with HTML time attributes", () => {
40 | render();
41 | const timeElement = screen.getByText("5 years ago");
42 | expect(timeElement.tagName).toBe("TIME");
43 | expect(timeElement.id).toBe("timeago");
44 | expect(timeElement.title).toBe("Nov 15, 2019");
45 | });
46 |
--------------------------------------------------------------------------------
/packages/demo/__tests__/setup.ts:
--------------------------------------------------------------------------------
1 | import { afterEach } from "vitest";
2 | import { cleanup } from "@testing-library/react";
3 |
4 | afterEach(() => {
5 | cleanup();
6 | });
7 |
--------------------------------------------------------------------------------
/packages/demo/eslint.config.js:
--------------------------------------------------------------------------------
1 | import tseslint from 'typescript-eslint';
2 | import reactPlugin from 'eslint-plugin-react';
3 | import js from '@eslint/js';
4 | import prettierConfig from 'eslint-plugin-prettier/recommended';
5 |
6 | /** @type {import('eslint').Linter.Config[]} */
7 | export default [
8 | {
9 | linterOptions: {
10 | reportUnusedDisableDirectives: true,
11 | },
12 | languageOptions: {
13 | parser: tseslint.parser,
14 | parserOptions: {
15 | project: './tsconfig.app.json'
16 | }
17 | },
18 | plugins: {
19 | '@typescript-eslint': tseslint.plugin,
20 | 'react': reactPlugin
21 | },
22 | settings: {
23 | react: {
24 | version: 'detect'
25 | }
26 | },
27 | },
28 | js.configs.recommended,
29 | ...tseslint.configs.recommended,
30 | reactPlugin.configs.flat.recommended,
31 | reactPlugin.configs.flat['jsx-runtime'],
32 | prettierConfig,
33 | ];
34 |
--------------------------------------------------------------------------------
/packages/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | timeago-react
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "start": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint src __tests__",
10 | "test": "node test-react-versions.js",
11 | "test:unit": "vitest run"
12 | },
13 | "dependencies": {
14 | "react": "^16.14.0",
15 | "react-dom": "^16.14.0",
16 | "timeago-react": "*"
17 | },
18 | "devDependencies": {
19 | "@testing-library/jest-dom": "^6.6.3",
20 | "@testing-library/react": "^12.1.5",
21 | "@types/react": "^16.14.62",
22 | "@types/react-dom": "^16.9.25",
23 | "@types/testing-library__jest-dom": "^5.14.9",
24 | "@vitejs/plugin-react": "^4.3.4",
25 | "eslint": "^9.21.0",
26 | "eslint-config-prettier": "^10.0.2",
27 | "eslint-plugin-prettier": "^5.2.3",
28 | "eslint-plugin-react": "^7.37.4",
29 | "jsdom": "^26.0.0",
30 | "typescript": "^5.8.2",
31 | "typescript-eslint": "^8.26.0",
32 | "vite": "^6.2.0",
33 | "vitest": "^3.0.7"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/demo/set-react-version.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { execSync } from 'child_process';
4 | import { readFileSync, writeFileSync, rmSync } from 'fs';
5 | import { resolve } from 'path';
6 |
7 | // Map of React versions to compatible @testing-library/react versions
8 | const TESTING_LIBRARY_VERSIONS = {
9 | '0.14': '^12.1.5',
10 | '15': '^12.1.5',
11 | '16': '^12.1.5',
12 | '17': '^12.1.5',
13 | '18': '^16.2.0',
14 | '19': '^16.2.0',
15 | };
16 |
17 | // Get version from command line argument
18 | const version = process.argv[2];
19 |
20 | if (!version || !Object.keys(TESTING_LIBRARY_VERSIONS).includes(version)) {
21 | console.error('Error: Invalid or missing React version number');
22 | console.error('Usage: node set-react-version.js ');
23 | console.error('Example: node set-react-version.js 18');
24 | console.error(`Valid options: [${Object.keys(TESTING_LIBRARY_VERSIONS).sort().join(', ')}]`);
25 | process.exit(1);
26 | }
27 |
28 | try {
29 | const testingLibraryVersion = TESTING_LIBRARY_VERSIONS[version];
30 | console.log(`Installing React version ${version}, corresponding types and compatible testing library version...`);
31 | execSync(`npm install react@${version} react-dom@${version} @types/react@${version} @types/react-dom@${version} @testing-library/react@${testingLibraryVersion} --force`, { stdio: 'inherit' });
32 |
33 | // In an ideal world we wouldn't need these two extra steps
34 | // Unfortunately NPM is often buggy and doens't actually install what we want
35 | console.log('Deleting node_modules and lock file to force install...');
36 | rmSync('./node_modules', { recursive: true, force: true });
37 | rmSync('../../node_modules', { recursive: true, force: true });
38 | rmSync('../../package-lock.json', { recursive: true, force: true });
39 | rmSync('../timeago-react/node_modules', { recursive: true, force: true });
40 | console.log(`Running npm install...`);
41 | execSync(`npm install`, { stdio: 'inherit' });
42 |
43 | // This is because when importing from this adjacent package, if it has a different react version in node_modules it will clash
44 | // But we'll default back to ours if it's gone
45 | // (note that npm install will restore this clashing file)
46 | console.log('Deleting clashing version from timeago-react/node_modules/...');
47 | rmSync('../timeago-react/node_modules', { recursive: true, force: true });
48 |
49 | // Special edits for 0.14
50 | // See https://github.com/testing-library/react-testing-library/issues/315
51 | if (version === '0.14') {
52 | console.log('Patching @testing-library/react for 0.14...');
53 | const filePath = resolve(new URL(import.meta.resolve('@testing-library/react')).pathname, '..', 'act-compat.js');
54 | const currentContent = readFileSync(filePath, 'utf8');
55 | writeFileSync(filePath, currentContent.replace(
56 | '_interopRequireWildcard(require("react-dom/test-utils"));',
57 | '{};',
58 | ));
59 | }
60 |
61 | console.log('Updating index.html to point at correct entrypoint...');
62 | const filePath = resolve('index.html');
63 | const currentContent = readFileSync(filePath, 'utf8');
64 | writeFileSync(filePath, currentContent.replace(
65 | /src\/main-(legacy|new)\.tsx/,
66 | version >= '18' ? 'src/main-new.tsx' : 'src/main-legacy.tsx',
67 | ));
68 |
69 | console.log(`\nSuccessfully installed React v${version} and compatible dependencies`);
70 | } catch (error) {
71 | console.error('\nError installing dependencies:', error.message);
72 | process.exit(1);
73 | }
74 |
--------------------------------------------------------------------------------
/packages/demo/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #f3f4fa;
3 | margin: 0 auto;
4 | max-width: 1000px;
5 | }
6 |
7 | html, body {
8 | -webkit-box-sizing: border-box;
9 | -moz-box-sizing: border-box;
10 | box-sizing: border-box;
11 | }
12 |
13 | *, *:before, *:after {
14 | -webkit-box-sizing: inherit;
15 | -moz-box-sizing: inherit;
16 | box-sizing: inherit;
17 | }
18 |
19 | body, input, button, textarea {
20 | font-family: Georgia, Helvetica;
21 | font-size: 17px;
22 | color: #005064;
23 | }
24 | h1 {
25 | margin-top: 20px;
26 | text-align: center;
27 | }
28 |
29 | h3 {
30 | background-color: hsla(0, 0%, 0%, 0.2);
31 | padding: 20px;
32 | text-align: center;
33 | }
34 |
35 | h4 {
36 | padding: 20px;
37 | text-align: center;
38 | color: black;
39 | }
40 |
41 | h4 a {
42 | color: black;
43 | }
44 |
45 | h4 a:hover {
46 | color: blue;
47 | }
48 |
49 | a,
50 | a:hover {
51 | color: red;
52 | }
53 |
54 | pre {
55 | background-color: #ead9d9;
56 | padding: 5px;
57 | }
58 | pre code {
59 | color: #111;
60 | font-size: 18px;
61 | }
62 |
63 | pre {
64 | overflow-x: auto;
65 | }
66 |
67 | label {
68 | display: block;
69 | margin-bottom: 15px;
70 | }
71 |
72 | sub {
73 | display: block;
74 | margin-top: -10px;
75 | margin-bottom: 15px;
76 | font-size: 11px;
77 | font-style: italic;
78 | }
79 |
80 | ul {
81 | margin: 0;
82 | padding: 0;
83 | }
84 |
85 | .parent {
86 | background-color: rgba(255, 255, 255, 0.2);
87 | margin: 20px 0;
88 | padding: 20px;
89 | }
90 |
91 | input {
92 | border: none;
93 | outline: none;
94 | background-color: #ecf0f1;
95 | padding: 10px;
96 | color: #14204f;
97 | border: 0;
98 | margin: 5px 0;
99 | display: block;
100 | width: 100%;
101 | }
102 |
103 | button {
104 | background-color: #ecf0f1;
105 | color: #14204f;
106 | border: 0;
107 | padding: 18px 12px;
108 | margin-left: 6px;
109 | cursor: pointer;
110 | outline: none;
111 | }
112 |
113 | button:hover {
114 | background-color: #e74c3c;
115 | color: #ecf0f1;
116 | }
117 |
118 | .a_line {
119 | display: block;
120 | width: 100%;
121 | }
122 |
123 | .nsg-tag {
124 | border-color: #14204f;
125 | }
126 |
127 | .inline {
128 | display: inline-block;
129 | }
130 |
131 | .gh-fork {
132 | position: fixed;
133 | top: 0;
134 | right: 0;
135 | border: 0;
136 | }
137 |
138 | .autofruit {
139 | width: 16px;
140 | margin-right: 3px;
141 | }
142 |
143 | #ddl {
144 | cursor: pointer;
145 | padding: 10px;
146 | background-color: rgba(0,0,0,0.1);
147 | }
148 |
149 | .examples {
150 | background-color: rgba(0,0,0,0.1);
151 | }
--------------------------------------------------------------------------------
/packages/demo/src/App.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
2 | import React from "react";
3 | import { register } from "timeago.js";
4 | import TimeAgo from "timeago-react";
5 | import viLang from "timeago.js/lib/lang/vi";
6 | import "./App.css";
7 |
8 | register("vi", viLang);
9 |
10 | function App() {
11 | return (
12 |
13 |
timeago-react
14 |
15 | A simple and efficient component to format date with `*** time ago`
16 | statement.
17 |
18 |
19 |
20 |
21 |
25 | You opened this page
26 |
27 |
28 |
29 | .
30 |
31 | {""}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
45 | Hustcc was born
46 |
47 |
48 |
49 | .
50 |
51 | {""}
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | You opened this page
60 |
61 |
62 |
63 | .
64 |
65 |
66 | {
67 | ""
68 | }
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
81 | Earth Day 2017 was
82 |
83 |
87 |
88 | .
89 |
90 |
91 | {
92 | ""
93 | }
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
106 | 2019-11-10 is
107 |
108 |
112 |
113 | relative to 2019-11-11.
114 |
115 |
116 | {
117 | ""
118 | }
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
131 |
132 |
137 |
138 |
139 |
140 | {
141 | ""
142 | }
143 |
144 |
145 |
146 |
147 |
148 |
149 | Download from GitHub!{" "}
150 | timeago-react
151 |
152 |
153 | );
154 | }
155 |
156 | export default App;
157 |
--------------------------------------------------------------------------------
/packages/demo/src/main-legacy.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
2 | import React, { StrictMode } from "react";
3 | import App from "./App.tsx";
4 | import { render } from "react-dom";
5 |
6 | const container = document.getElementById("root")!;
7 | render(
8 |
9 |
10 | ,
11 | container,
12 | );
13 |
--------------------------------------------------------------------------------
/packages/demo/src/main-new.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
2 | import React, { StrictMode } from "react";
3 | import App from "./App.tsx";
4 | import { createRoot } from "react-dom/client";
5 |
6 | const container = document.getElementById("root")!;
7 | createRoot(container).render(
8 |
9 |
10 | ,
11 | );
12 |
--------------------------------------------------------------------------------
/packages/demo/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/demo/test-react-versions.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { execSync } from 'child_process';
4 |
5 | const REACT_VERSIONS_TO_TEST = [
6 | '0.14',
7 | '15',
8 | '16',
9 | '17',
10 | '18',
11 | '19',
12 | ];
13 |
14 | REACT_VERSIONS_TO_TEST.forEach((version) => {
15 | console.log(`==== Starting test for React version ${version} ====`);
16 | execSync(`node set-react-version.js ${version}`, { stdio: 'inherit' });
17 | execSync(`npm run test:unit`, { stdio: 'inherit' });
18 | execSync(`npm run build`, { stdio: 'inherit' });
19 | })
20 |
21 | // Set things back to default React version
22 | execSync(`node set-react-version.js 16`, { stdio: 'inherit' });
23 |
24 | console.log('==== All tests passed! ====');
25 |
--------------------------------------------------------------------------------
/packages/demo/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src", "__tests__"]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/packages/demo/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/packages/demo/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | export default defineConfig({
5 | base: './',
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/packages/demo/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 | import react from '@vitejs/plugin-react';
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | test: {
7 | environment: 'jsdom',
8 | globals: true,
9 | setupFiles: ['__tests__/setup.ts'],
10 | include: ['__tests__/**/*.test.{ts,tsx}'],
11 | coverage: {
12 | provider: 'v8',
13 | include: ['src/**/*'],
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/packages/timeago-react/.gitignore:
--------------------------------------------------------------------------------
1 | # copied from root
2 | README.md
3 | LICENSE
4 |
--------------------------------------------------------------------------------
/packages/timeago-react/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "bracketSpacing": true,
6 | "printWidth": 120,
7 | "arrowParens": "always"
8 | }
9 |
--------------------------------------------------------------------------------
/packages/timeago-react/__tests__/setup.ts:
--------------------------------------------------------------------------------
1 | import { afterEach } from 'vitest';
2 | import { cleanup } from '@testing-library/react';
3 |
4 | afterEach(() => {
5 | cleanup();
6 | });
7 |
--------------------------------------------------------------------------------
/packages/timeago-react/__tests__/timeago-react.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import { test, expect } from 'vitest';
4 | import TimeAgo from '../src/timeago-react';
5 |
6 | const formatDate = (date: Date, fmt: string): string => {
7 | const o = {
8 | 'M+': date.getMonth() + 1, //month
9 | 'd+': date.getDate(), //day
10 | 'h+': date.getHours(), //hour
11 | 'm+': date.getMinutes(), //minute
12 | 's+': date.getSeconds(), //second
13 | 'q+': Math.floor((date.getMonth() + 3) / 3), //quarter
14 | S: date.getMilliseconds(), //millisecond
15 | };
16 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
17 | for (const k in o)
18 | if (new RegExp('(' + k + ')').test(fmt))
19 | fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length));
20 | return fmt;
21 | };
22 |
23 | test('tests will throw if checking for invalid text', () => {
24 | const d = +new Date() - 3601 * 1000; // 1 hour ago
25 | render();
26 | expect(() => screen.getByText('not in document')).toThrow();
27 | });
28 |
29 | test('renders timeago with timestamp', () => {
30 | const d = +new Date() - 3601 * 1000; // 1 hour ago
31 | render();
32 | screen.getByText('1 hour ago');
33 | });
34 |
35 | test('renders timeago with Date instance', () => {
36 | const d = +new Date() - 24 * 3601 * 1000; // 1 day ago
37 | render();
38 | screen.getByText('1 day ago');
39 | });
40 |
41 | test('renders timeago with Date string', () => {
42 | const d = +new Date() - 24 * 3601 * 1000; // 1 day ago
43 | render();
44 | screen.getByText('1 day ago');
45 | });
46 |
47 | test('renders timeago with locale', () => {
48 | const d = +new Date() - 3601 * 1000; // an hour ago
49 | render();
50 | screen.getByText('1 小时前');
51 | });
52 |
53 | test('renders timeago with className', () => {
54 | const d = new Date();
55 | const { container } = render();
56 | const timeElement = container.querySelector('time');
57 | expect(timeElement).not.toBeNull();
58 | expect(timeElement?.classList.contains('my_classname')).toBe(true);
59 | });
60 |
61 | test('renders timeago with relative date option', () => {
62 | render();
63 | screen.getByText('1 天前');
64 | });
65 |
--------------------------------------------------------------------------------
/packages/timeago-react/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import tseslint from 'typescript-eslint';
2 | import reactPlugin from 'eslint-plugin-react';
3 | import js from '@eslint/js';
4 | import prettierConfig from 'eslint-plugin-prettier/recommended';
5 |
6 | /** @type {import('eslint').Linter.Config[]} */
7 | export default [
8 | {
9 | linterOptions: {
10 | reportUnusedDisableDirectives: true,
11 | },
12 | languageOptions: {
13 | parser: tseslint.parser,
14 | parserOptions: {
15 | project: './tsconfig.ci.json'
16 | }
17 | },
18 | plugins: {
19 | '@typescript-eslint': tseslint.plugin,
20 | 'react': reactPlugin
21 | },
22 | settings: {
23 | react: {
24 | version: 'detect'
25 | }
26 | },
27 | },
28 | js.configs.recommended,
29 | ...tseslint.configs.recommended,
30 | reactPlugin.configs.flat.recommended,
31 | prettierConfig,
32 | ];
33 |
--------------------------------------------------------------------------------
/packages/timeago-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "timeago-react",
3 | "version": "3.0.7",
4 | "description": "timeago-react is a simple(only 1kb) react component used to format date with `*** time ago` statement. eg: '3 hours ago'.",
5 | "main": "cjs/timeago-react.js",
6 | "module": "esm/timeago-react.js",
7 | "files": [
8 | "src",
9 | "cjs",
10 | "esm"
11 | ],
12 | "scripts": {
13 | "lint": "eslint src __tests__",
14 | "lint-staged": "lint-staged",
15 | "test": "vitest run --coverage",
16 | "build": "npm run build:cjs && npm run build:esm && node postbuild.js",
17 | "build:cjs": "shx rm -rf ./cjs && tsc --module commonjs --outDir cjs",
18 | "build:esm": "shx rm -rf ./esm && tsc --module ESNext --outDir esm",
19 | "prepublishOnly": "npm run build"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/hustcc/timeago-react.git"
24 | },
25 | "keywords": [
26 | "react",
27 | "component",
28 | "timeago-react",
29 | "react-timeago",
30 | "timeago.js",
31 | "timeago",
32 | "react-component"
33 | ],
34 | "author": "hustcc (http://github.com/hustcc)",
35 | "license": "MIT",
36 | "bugs": {
37 | "url": "https://github.com/hustcc/timeago-react/issues"
38 | },
39 | "homepage": "https://github.com/hustcc/timeago-react",
40 | "devDependencies": {
41 | "@commitlint/cli": "^19.7.1",
42 | "@eslint/js": "^9.21.0",
43 | "@testing-library/react": "^12.1.5",
44 | "@types/react": "^16.14.62",
45 | "@types/react-dom": "^16.9.25",
46 | "@vitejs/plugin-react": "^4.3.4",
47 | "@vitest/coverage-v8": "^3.0.7",
48 | "eslint": "^9.21.0",
49 | "eslint-config-prettier": "^10.0.2",
50 | "eslint-plugin-prettier": "^5.2.3",
51 | "eslint-plugin-react": "^7.37.4",
52 | "jsdom": "^26.0.0",
53 | "lint-staged": "^15.4.3",
54 | "prettier": "^3.5.3",
55 | "react": "^16.14.0",
56 | "react-dom": "^16.14.0",
57 | "shx": "^0.3.4",
58 | "typescript": "^5.8.2",
59 | "typescript-eslint": "^8.26.0",
60 | "vitest": "^3.0.7"
61 | },
62 | "dependencies": {
63 | "timeago.js": "^4.0.0"
64 | },
65 | "peerDependencies": {
66 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
67 | },
68 | "lint-staged": {
69 | "src/**/*.{js,jsx,ts,tsx}": [
70 | "eslint --fix",
71 | "prettier --write"
72 | ]
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/packages/timeago-react/postbuild.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { copyFileSync, readFileSync, writeFileSync } from 'fs';
4 |
5 | copyFileSync('../../README.md', './README.md');
6 | copyFileSync('../../LICENSE', './LICENSE');
7 |
8 | // This enables backwards compatibility with React 0.14, as PureComponent was introduced in React 15
9 | ['esm', 'cjs'].forEach((dir) => {
10 | const filePath = `./${dir}/timeago-react.js`
11 | const currentContent = readFileSync(filePath, 'utf8');
12 | writeFileSync(filePath, currentContent.replace(
13 | 'React.PureComponent',
14 | 'React.PureComponent || React.Component',
15 | ));
16 | })
17 |
--------------------------------------------------------------------------------
/packages/timeago-react/src/timeago-react.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { format, cancel, render } from 'timeago.js';
3 | import { Opts, TDate } from 'timeago.js/lib/interface';
4 | export { Opts, TDate };
5 |
6 | /**
7 | * Convert input to a valid datetime string of