├── .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 | [![npm](https://img.shields.io/npm/v/timeago-react.svg)](https://www.npmjs.com/package/timeago-react) 14 | [![build](https://github.com/hustcc/timeago-react/workflows/ci/badge.svg)](https://github.com/hustcc/timeago-react) 15 | [![demo](https://github.com/hustcc/timeago-react/workflows/demo/badge.svg)](https://github.com/hustcc/timeago-react) 16 | [![npm](https://img.shields.io/npm/dm/timeago-react.svg)](https://www.npmjs.com/package/timeago-react) 17 | [![react supported](https://img.shields.io/badge/React-%5E0.14.0%20%7C%7C%20%5E15.0.0%20%7C%7C%20%5E16.0.0%20%7C%7C%20%5E17.0.0%20%7C%7C%20%5E18.0.0%20%7C%7C%20%5E19.0.0-blue.svg)](https://github.com/hustcc/timeago-react) 18 | [![npm](https://img.shields.io/npm/l/timeago-react.svg)](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