├── .editorconfig
├── .eslintrc.js
├── .github
└── workflows
│ ├── deploy-gh-page.yml
│ ├── publish.yml
│ └── quality.yml
├── .gitignore
├── .npmrc
├── README.md
├── bin
└── docker.sh
├── example
└── index.html
├── package.json
├── src
├── App.ts
├── Configuration
│ ├── Configuration.ts
│ └── ConfigurationValidator.ts
├── DateInterval
│ ├── DateInterval.ts
│ └── DateIntervalCalculator.ts
├── Item
│ └── Item.ts
├── Locale
│ └── LocaleManager.ts
├── Rule
│ ├── Rule.ts
│ ├── RuleInterpreter.ts
│ └── RuleRender.ts
├── defaultRules.ts
└── index.ts
├── tsconfig.json
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 | max_line_length = 100
10 |
11 | [{*.js, *.ts}]
12 | indent_size = 2
13 |
14 | [{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,composer.lock,jest.config}]
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | plugins: ['@typescript-eslint'],
4 | extends: [
5 | 'eslint:recommended',
6 | 'airbnb-base',
7 | "plugin:@typescript-eslint/eslint-recommended",
8 | 'plugin:@typescript-eslint/recommended',
9 | ],
10 | rules: {
11 | 'import/extensions': ['error', {
12 | 'ts': 'never',
13 | 'json': 'always'
14 | }],
15 | 'class-methods-use-this': 'off',
16 | 'no-restricted-syntax': 'off',
17 | 'no-param-reassign': 'off',
18 | 'no-cond-assign': 'off',
19 | 'no-useless-escape': 'off',
20 | "no-useless-constructor": "off",
21 | "@typescript-eslint/no-useless-constructor": ["error"],
22 | 'quotes': 'error',
23 | 'semi': 'error',
24 | 'comma-dangle': ['error', {
25 | 'arrays': 'always',
26 | }],
27 | 'arrow-parens': ['error', 'as-needed'],
28 | 'max-len': 'off',
29 | },
30 | env: {
31 | browser: true,
32 | },
33 | settings: {
34 | 'import/resolver': 'webpack',
35 | },
36 | parserOptions: {
37 | ecmaVersion: 2018,
38 | sourceType: 'module',
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-gh-page.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - master
5 |
6 | jobs:
7 | deploy-gh-page:
8 | name: deploy-gh-page
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v1
12 | - run: mkdir ./example/clever-date
13 | - run: npm install
14 | - run: npm run build
15 | - run: cp -r ./dist/* ./example/
16 | - name: deploy
17 | uses: peaceiris/actions-gh-pages@v2
18 | env:
19 | ACTIONS_DEPLOY_KEY: ${{ secrets.PUBLISH_DIST_DEPLOY_KEY }}
20 | PUBLISH_BRANCH: gh-pages
21 | PUBLISH_DIR: ./example
22 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*'
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v1
13 | - uses: actions/setup-node@v1
14 | with:
15 | node-version: 12
16 | registry-url: https://registry.npmjs.org/
17 | - run: npm install
18 | - run: npm version "${GITHUB_REF:11}" --git-tag-version false --commit-hooks false
19 | - run: npm publish --access public
20 | env:
21 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
22 |
--------------------------------------------------------------------------------
/.github/workflows/quality.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - '*' # matches every branch
5 | - '*/*' # matches every branch containing a single '/'
6 | jobs:
7 | eslint:
8 | name: eslint
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Use Node.js
13 | uses: actions/setup-node@v1
14 | with:
15 | node-version: 12.x
16 | - run: npm install
17 | - run: npm run eslint
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
4 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Clever Date
2 |
3 | 
4 | 
5 | 
6 |
7 | A javascript library (<10kB) to show an intelligent date refreshed at regular intervals.
8 |
9 | ## Demo
10 | A demo is available [here](https://date-js.github.io/clever-date/).
11 |
12 | ## Supported languages
13 | Languages defined bellow are fully supported but you can add your own rules with other languages.
14 | - English
15 | - Français
16 |
17 | You can also contribute and suggest translations with a pull request.
18 |
19 | ## Example
20 |
21 | Add an attribute to your date with the corresponding timestamp.
22 | ``` html
23 |
26/01/2020 12h12
24 | 25/01/2020 12h12
25 | ```
26 |
27 | Start the script:
28 | ``` javascript
29 | CleverDate.start();
30 | ```
31 |
32 | Let's see the result:
33 | ``` html
34 | 2 minutes ago
35 | Yesterday at 12:12
36 | ```
37 |
38 | ### Some possible results:
39 | - Just now / A l'instant
40 | - 2 minutes ago / Il y a 2 minutes
41 | - 2 hours ago / Il y a 2 heures
42 | - Today at 11:46 / Aujourd'hui à 11h46
43 | - Yesterday at 11:46 / Hier à 11h46
44 |
45 | ## Install
46 |
47 | ### ES6
48 |
49 | ``` bash
50 | npm install @date-js/clever-date
51 | ```
52 |
53 | ``` javascript
54 | import CleverDate from '@date-js/clever-date';
55 | ```
56 |
57 | ### Otherwise
58 |
59 | ``` html
60 |
61 | ```
62 |
63 | ## Usage
64 | Start the process :
65 | ``` javascript
66 | CleverDate.start();
67 | ```
68 |
69 | Stop the process:
70 | ``` javascript
71 | CleverDate.stop();
72 | ```
73 |
74 | If you add element dynamically.
75 | ``` javascript
76 | window.dispatch(new Event('clever-date.update'));
77 | ```
78 |
79 | ## Customize it
80 |
81 | You have the full possibility to customize your rules by passing your configuration.
82 |
83 | ``` javascript
84 | var configuration = {
85 | refresh: 5, // The minimal refreshing time
86 | selector: 'data-clever-date', // Elements with this attribute will be parsed
87 | rules: [
88 | {
89 | condition: function(dateIntervalItem) { return dateIntervalItem.day >= 365*10; }, text: {
90 | fr: "Il y a %dd jour{%dd||s} (année %Y)",
91 | en: "%dd day{%dd||s} ago (year %Y)"
92 | }
93 | }
94 | ]
95 | }
96 |
97 | CleverDate.start(configuration);
98 | ```
99 |
100 | ## How create rules ?
101 |
102 | You can see examples in [default rules file](src/defaultRules.ts).
103 |
104 | A rule is composed of some elements:
105 |
106 | ##### condition
107 | A callback which returns true if the rule matches.\
108 | DateInterval is injected in the callback: see [DateInterval.ts](src/DateInterval/DateInterval.ts) for more information.
109 |
110 | ##### refresh
111 | For improving performances, it's not necessary to analyse and parse your rule each time.
112 | - null: never analysed again
113 | - undefined: use default refreshing time
114 | - number: seconds between analyses
115 |
116 | ##### text
117 | An object with the text for each language that you want to target.
118 |
--------------------------------------------------------------------------------
/bin/docker.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | DIRECTORY=$(dirname $(realpath $0 ))
4 |
5 | docker run -it --rm \
6 | --name clever-date \
7 | -v "$DIRECTORY/..":/home/node/app \
8 | -w /home/node/app \
9 | -p 8080:8080 \
10 | -u $(id -u ${USER}):$(id -g ${USER}) \
11 | node:14 \
12 | bash
13 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Clever Date
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Clever Date
13 |
Date is refreshed at regular intervals.
14 |
15 |
16 |
17 | - Just now
18 | - Some seconds
19 | - Some minutes
20 | - Some hours
21 | - Today
22 | - Yesterday
23 | - 01/01/2018 10h30
24 | - 01/01/2008 10h30
25 |
26 |
27 |
28 |
Refresh
29 |
30 |
31 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@date-js/clever-date",
3 | "version": "0.0.0-version",
4 | "description": "A Javascript module to show an intelligent date refreshed at regular intervals.",
5 | "main": "dist/clever-date.js",
6 | "scripts": {
7 | "prepublishOnly": "webpack --mode production",
8 | "eslint": "eslint ./src --ext .ts,.js,.d.ts",
9 | "eslint-fix": "eslint ./src --ext .ts,.js,.d.ts --fix",
10 | "watch": "webpack --watch",
11 | "build": "webpack",
12 | "start:dev": "webpack serve --mode development"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/date-js/clever-date.git"
17 | },
18 | "keywords": [
19 | "date",
20 | "hour",
21 | "day",
22 | "month",
23 | "year",
24 | "intelligent",
25 | "clever",
26 | "refresh",
27 | "realtime",
28 | "human",
29 | "pretty",
30 | "time",
31 | "update",
32 | "tiny",
33 | "light"
34 | ],
35 | "files": [
36 | "dist"
37 | ],
38 | "typings": "dist/types/index.d.ts",
39 | "author": "Adrien Peyre",
40 | "license": "MIT",
41 | "bugs": {
42 | "url": "https://github.com/date-js/clever-date/issues"
43 | },
44 | "homepage": "https://github.com/date-js/clever-date#readme",
45 | "devDependencies": {
46 | "@babel/core": "^7.14.6",
47 | "@babel/preset-env": "^7.14.7",
48 | "@babel/preset-typescript": "^7.14.5",
49 | "@types/node": "^12.20.15",
50 | "@typescript-eslint/eslint-plugin": "^2.34.0",
51 | "@typescript-eslint/parser": "^2.34.0",
52 | "babel-loader": "^8.2.2",
53 | "eslint": "^6.8.0",
54 | "eslint-config-airbnb-base": "^14.2.1",
55 | "eslint-config-prettier": "^6.15.0",
56 | "eslint-config-standard": "^14.1.1",
57 | "eslint-import-resolver-webpack": "^0.12.2",
58 | "eslint-plugin-import": "^2.23.4",
59 | "eslint-plugin-promise": "^4.3.1",
60 | "terser-webpack-plugin": "^5.1.4",
61 | "ts-loader": "^9.2.3",
62 | "typescript": "^4.3.4",
63 | "webpack": "^5.41.0",
64 | "webpack-cli": "^4.7.2",
65 | "webpack-dev-server": "^4.0.0-beta.3"
66 | },
67 | "dependencies": {
68 | "@date-js/date-formatter": "^1.0.1"
69 | },
70 | "babel": {
71 | "presets": [
72 | "@babel/preset-env",
73 | "@babel/preset-typescript"
74 | ]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/App.ts:
--------------------------------------------------------------------------------
1 | import Configuration from './Configuration/Configuration';
2 | import Item from './Item/Item';
3 | import DateIntervalCalculator from './DateInterval/DateIntervalCalculator';
4 | import RuleInterpreter from './Rule/RuleInterpreter';
5 | import Rule from './Rule/Rule';
6 | import defaultRules from './defaultRules';
7 | import ConfigurationValidator from './Configuration/ConfigurationValidator';
8 | import RuleRender from './Rule/RuleRender';
9 | import LocaleManager from './Locale/LocaleManager';
10 |
11 | export default class App {
12 | private static readonly EVENT_REFRESH = 'clever-date.update';
13 |
14 | private items: Item[] = [];
15 |
16 | private rules: Rule[] = [];
17 |
18 | private timer: number | null = null;
19 |
20 | private readonly locale: string;
21 |
22 | public constructor() {
23 | this.locale = LocaleManager.getLocaleName();
24 | }
25 |
26 | public start(userConfig: Configuration|null = null): void {
27 | const config = { ...new Configuration(), ...userConfig };
28 | config.rules = config.rules.concat(defaultRules);
29 |
30 | const error = ConfigurationValidator.validate(config);
31 | if (error instanceof Error) {
32 | throw error;
33 | }
34 |
35 | this.rules = config.rules;
36 |
37 | const execProcess = (): void => {
38 | this.extractItems(config.selector);
39 | this.analyse();
40 | };
41 |
42 | if (this.timer === null) {
43 | this.timer = this.startTimer(config.refresh, execProcess);
44 | }
45 |
46 | window.addEventListener(App.EVENT_REFRESH, () => execProcess());
47 |
48 | execProcess();
49 | }
50 |
51 | public stop(): void {
52 | if (this.timer !== null) {
53 | window.clearInterval(this.timer);
54 | }
55 | }
56 |
57 | private extractItems(selector: string): void {
58 | const matches = document.querySelectorAll(`[${selector}]`);
59 |
60 | matches.forEach(element => {
61 | let attributeValue: string|number = element.getAttribute(selector) as string;
62 |
63 | if (!Number.isNaN(Number(attributeValue))) {
64 | const timestamp = parseInt(attributeValue, 10);
65 | attributeValue = timestamp * 1000;
66 | }
67 |
68 | const itemDate = new Date(attributeValue);
69 |
70 | if (itemDate.getTime() > 0) {
71 | this.items.push(new Item(element, element.innerHTML, itemDate));
72 | }
73 |
74 | element.removeAttribute(selector);
75 | if (!element.hasAttribute('title')) {
76 | element.setAttribute('title', element.textContent as string);
77 | }
78 | });
79 | }
80 |
81 | private analyse(): void {
82 | const currentDate = new Date();
83 |
84 | const itemsToAnalyse = this.items.filter(
85 | item => item.nextUpdate === undefined || (item.nextUpdate !== null && item.nextUpdate < currentDate)
86 | );
87 |
88 | itemsToAnalyse.forEach(item => this.manageItem(item));
89 | }
90 |
91 | private manageItem(item: Item): void {
92 | const dateInterval = DateIntervalCalculator.getDateInterval(item.date);
93 | const ruleResult = RuleInterpreter.render(this.rules, dateInterval, this.locale);
94 |
95 | const setHtml = (newContent: string): void => {
96 | if (item.reference.innerHTML !== newContent) {
97 | item.reference.innerHTML = newContent;
98 | }
99 | };
100 |
101 | if (ruleResult instanceof RuleRender) {
102 | if (ruleResult.nextUpdate === null) {
103 | this.removeItem(item);
104 | }
105 |
106 | item.nextUpdate = ruleResult.nextUpdate;
107 | setHtml(ruleResult.render);
108 | } else {
109 | setHtml(item.initialContent);
110 | }
111 | }
112 |
113 | private startTimer(interval: number, callback: () => void): number {
114 | return window.setInterval(() => {
115 | callback();
116 | }, interval * 1000);
117 | }
118 |
119 | private removeItem(itemToRemove: Item): void {
120 | const index = this.items.findIndex(item => item === itemToRemove);
121 | if (index >= 0) {
122 | this.items.splice(index, 1);
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/Configuration/Configuration.ts:
--------------------------------------------------------------------------------
1 | import Rule from '../Rule/Rule';
2 |
3 | export default class Configuration {
4 | /**
5 | * Interval between new analyse (in seconds)
6 | */
7 | public refresh = 5;
8 |
9 | /**
10 | * Selector used to find date to manage
11 | */
12 | public selector = 'data-clever-date';
13 |
14 | /**
15 | * Set of rules merged in default rules
16 | */
17 | public rules: Rule[] = [];
18 | }
19 |
--------------------------------------------------------------------------------
/src/Configuration/ConfigurationValidator.ts:
--------------------------------------------------------------------------------
1 | import Configuration from './Configuration';
2 |
3 | export default class ConfigurationValidator {
4 | public static validate(config: Configuration): Error | null {
5 | if (typeof config.refresh !== 'number') {
6 | return new Error('Refresh value must be a number.');
7 | }
8 |
9 | for (const rule of config.rules) {
10 | if (!rule.condition || !rule.text || typeof rule.text !== 'object') {
11 | return new Error('A rule must be composed of a condition and a corresponding text keys.');
12 | }
13 | }
14 |
15 | return null;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/DateInterval/DateInterval.ts:
--------------------------------------------------------------------------------
1 | export default class DateInterval {
2 | public constructor(
3 | public date: Date,
4 | public day: number,
5 | public hour: number,
6 | public minute: number,
7 | public second: number
8 | ) {
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/DateInterval/DateIntervalCalculator.ts:
--------------------------------------------------------------------------------
1 | import DateInterval from './DateInterval';
2 |
3 | export default class DateIntervalCalculator {
4 | private static readonly SECONDS_IN_DAY = 86400;
5 |
6 | private static readonly SECONDS_IN_HOUR = 3600;
7 |
8 | private static readonly SECONDS_IN_MINUTE = 60;
9 |
10 | public static getDateInterval(date: Date): DateInterval {
11 | let diff = (Date.now() - date.getTime()) / 1000;
12 |
13 | const diffDays = Math.trunc(diff / DateIntervalCalculator.SECONDS_IN_DAY);
14 | diff -= DateIntervalCalculator.SECONDS_IN_DAY * diffDays;
15 |
16 | const diffHours = Math.trunc(diff / DateIntervalCalculator.SECONDS_IN_HOUR);
17 | diff -= DateIntervalCalculator.SECONDS_IN_HOUR * diffHours;
18 |
19 | const diffMinutes = Math.trunc(diff / DateIntervalCalculator.SECONDS_IN_MINUTE);
20 | diff -= DateIntervalCalculator.SECONDS_IN_MINUTE * diffMinutes;
21 |
22 | const diffSeconds = Math.trunc(diff);
23 |
24 | return new DateInterval(date, diffDays, diffHours, diffMinutes, diffSeconds);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Item/Item.ts:
--------------------------------------------------------------------------------
1 | export default class Item {
2 | public reference: Element;
3 |
4 | public initialContent: string;
5 |
6 | public date: Date;
7 |
8 | public nextUpdate: Date | null | undefined;
9 |
10 | public constructor(reference: Element, initialContent: string, date: Date) {
11 | this.reference = reference;
12 | this.initialContent = initialContent;
13 | this.date = date;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Locale/LocaleManager.ts:
--------------------------------------------------------------------------------
1 | export default class LocaleManager {
2 | public static getLocaleName(): string {
3 | return navigator.language.split('-')[0];
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/Rule/Rule.ts:
--------------------------------------------------------------------------------
1 | import DateInterval from '../DateInterval/DateInterval';
2 |
3 | export default class Rule {
4 | public condition: (dateInterval: DateInterval) => boolean = () => false;
5 |
6 | public text: { [key: string]: string } = {}
7 |
8 | /**
9 | * =number => refresh each n seconds
10 | * =null => No refresh needed
11 | * =undefined => Refresh following global refresh
12 | */
13 | public refresh?: number | null | undefined = undefined;
14 | }
15 |
--------------------------------------------------------------------------------
/src/Rule/RuleInterpreter.ts:
--------------------------------------------------------------------------------
1 | import DateFormatter from '@date-js/date-formatter';
2 | import DateInterval from '../DateInterval/DateInterval';
3 | import Rule from './Rule';
4 | import RuleRender from './RuleRender';
5 |
6 | class RuleInterpreter {
7 | public render(rules: Rule[], itemDateInterval: DateInterval, locale: string): RuleRender | null {
8 | const ruleFound: Rule|undefined = rules.find(
9 | rule => rule.condition(itemDateInterval) && locale in rule.text
10 | );
11 |
12 | if (!ruleFound) {
13 | return null;
14 | }
15 |
16 | return new RuleRender(
17 | this.parseRule(ruleFound.text[locale], itemDateInterval, locale),
18 | this.calculateNextUpdate(ruleFound)
19 | );
20 | }
21 |
22 | private parseRule(text: string, diff: DateInterval, locale: string): string {
23 | let parsedText = this.parseConditions(text, text, diff, locale);
24 | parsedText = this.parseValues(text, parsedText, diff, locale);
25 |
26 | return parsedText.replace('\\', '');
27 | }
28 |
29 | private parseConditions(
30 | text: string,
31 | parsedText: string,
32 | diff: DateInterval,
33 | locale: string
34 | ): string {
35 | const regexp = /{%([a-z]+)\|([^|\[\]]*)\|([^|\[\]]*)}/gi;
36 | let condData;
37 | while ((condData = regexp.exec(text)) !== null) {
38 | const condValue = this.getValueFromSymbol(condData[1], diff, locale);
39 | if (condValue !== null) {
40 | parsedText = parsedText.replace(
41 | condData[0],
42 | parseInt(condValue, 10) <= 1 ? condData[2] : condData[3]
43 | );
44 | }
45 | }
46 |
47 | return parsedText;
48 | }
49 |
50 | private parseValues(
51 | text: string,
52 | parsedText: string,
53 | diff: DateInterval,
54 | locale: string
55 | ): string {
56 | let varData;
57 | const varRegexp = /%([a-z]+)/gi;
58 | while ((varData = varRegexp.exec(text)) !== null) {
59 | const variableValue = this.getValueFromSymbol(varData[1], diff, locale);
60 | if (variableValue !== null) {
61 | parsedText = parsedText.replace(varData[0], variableValue);
62 | }
63 | }
64 |
65 | return parsedText;
66 | }
67 |
68 | private calculateNextUpdate(rule: Rule): Date | null | undefined {
69 | if (rule.refresh === null) {
70 | return null;
71 | }
72 |
73 | if (rule.refresh === undefined) {
74 | return undefined;
75 | }
76 |
77 | const currentDate = new Date();
78 | return new Date(currentDate.setTime(currentDate.getTime() + rule.refresh * 1000));
79 | }
80 |
81 | private getValueFromSymbol(symbol: string, diff: DateInterval, locale: string): string | null {
82 | switch (symbol) {
83 | case 'dd':
84 | return diff.day.toString();
85 | case 'dh':
86 | return diff.hour.toString();
87 | case 'dm':
88 | return diff.minute.toString();
89 | case 'ds':
90 | return diff.second.toString();
91 | default:
92 | return DateFormatter.getValueFromSymbol(symbol, diff.date, locale);
93 | }
94 | }
95 | }
96 |
97 | export default new RuleInterpreter();
98 |
--------------------------------------------------------------------------------
/src/Rule/RuleRender.ts:
--------------------------------------------------------------------------------
1 | export default class RuleRender {
2 | public constructor(public render: string, public nextUpdate: Date | null | undefined) {
3 | }
4 | }
5 |
--------------------------------------------------------------------------------
/src/defaultRules.ts:
--------------------------------------------------------------------------------
1 | import DateInterval from './DateInterval/DateInterval';
2 |
3 | const justNow = (itemDate: DateInterval): boolean => itemDate.day === 0 && itemDate.hour === 0 && itemDate.minute === 0 && itemDate.second >= 0 && itemDate.second < 30;
4 |
5 | const someSeconds = (itemDate: DateInterval): boolean => itemDate.day === 0 && itemDate.hour === 0 && itemDate.minute === 0 && itemDate.second >= 0 && itemDate.second < 60;
6 |
7 | const someMinutes = (itemDate: DateInterval): boolean => itemDate.day === 0 && itemDate.hour === 0 && itemDate.minute >= 0 && itemDate.minute < 60;
8 |
9 | const someHours = (itemDate: DateInterval): boolean => itemDate.day === 0 && itemDate.hour > 0 && itemDate.hour >= 0 && itemDate.hour < 12;
10 |
11 | const today = (itemDate: DateInterval): boolean => (new Date()).toDateString() === itemDate.date.toDateString();
12 |
13 | const yesterday = (itemDate: DateInterval): boolean => {
14 | const currentDate = new Date();
15 | const currentDateYesterday = currentDate.setDate(currentDate.getDate() - 1);
16 | return new Date(currentDateYesterday).toDateString() === itemDate.date.toDateString();
17 | };
18 |
19 | const someDays = (itemDate: DateInterval): boolean => itemDate.day > 0 && itemDate.day < 7;
20 |
21 | const sameYear = (itemDate: DateInterval): boolean => itemDate.date.getFullYear() === (new Date()).getFullYear();
22 |
23 | const otherwise = (): boolean => true;
24 |
25 | export default [
26 | {
27 | condition: justNow,
28 | text: {
29 | fr: 'à l\'instant',
30 | en: 'just now'
31 | }
32 | },
33 | {
34 | condition: someSeconds,
35 | text: {
36 | en: '%ds second{%ds||s} ago',
37 | fr: 'il y a %ds seconde{%ds||s}'
38 | }
39 | },
40 | {
41 | condition: someMinutes,
42 | text: {
43 | en: '%dm minute{%dm||s} ago',
44 | fr: 'il y a %dm minute{%dm||s}'
45 | }
46 | },
47 | {
48 | condition: someHours,
49 | refresh: 300,
50 | text: {
51 | en: '%dh hour{%dh||s} ago',
52 | fr: 'il y a %dh heure{%dh||s}'
53 | }
54 | },
55 | {
56 | condition: today,
57 | refresh: 60,
58 | text: {
59 | en: 'today at %H:%i',
60 | fr: 'aujourd\'hui à %H\\h%i'
61 | }
62 | },
63 | {
64 | condition: yesterday,
65 | refresh: 60,
66 | text: {
67 | en: 'yesterday at %H:%i',
68 | fr: 'hier à %H\\h%i'
69 | }
70 | },
71 | {
72 | condition: someDays,
73 | refresh: 3600,
74 | text: {
75 | en: '%l at %H:%i',
76 | fr: '%l à %H\\h%i'
77 | }
78 | },
79 | {
80 | condition: sameYear,
81 | refresh: null,
82 | text: {
83 | en: '%F %d',
84 | fr: 'le %d{%d|er|} %F'
85 | }
86 | },
87 | {
88 | condition: otherwise,
89 | refresh: null,
90 | text: {
91 | en: '%F %d, %Y',
92 | fr: 'le %j{%d|er|} %F %Y'
93 | }
94 | },
95 | ];
96 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import App from './App';
2 |
3 | const app = new App();
4 |
5 | const CleverDate = {
6 | start: app.start.bind(app),
7 | stop: app.stop.bind(app)
8 | };
9 |
10 | module.exports = CleverDate;
11 | export default CleverDate;
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "outDir": "./dist/",
5 | "declaration": true,
6 | "declarationDir": "./dist/types",
7 | "noImplicitAny": true,
8 | "moduleResolution": "node",
9 | "module": "commonjs",
10 | "target": "es5",
11 | "resolveJsonModule": true,
12 | "esModuleInterop": true,
13 | "lib": [
14 | "es2015",
15 | "dom"
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require('terser-webpack-plugin');
3 |
4 | const config = {
5 | mode: 'production',
6 | entry: {
7 | 'index': './src/index.ts',
8 | },
9 | output: {
10 | filename: 'clever-date.js',
11 | path: path.resolve(__dirname, 'dist'),
12 | library: {
13 | name: 'CleverDate',
14 | type: 'umd',
15 | },
16 | },
17 | resolve: {
18 | extensions: ['.ts', '.js', '.json'],
19 | },
20 | module: {
21 | rules: [
22 | {
23 | test: /\.ts$/,
24 | exclude: /(node_modules)/,
25 | use: {
26 | loader: 'babel-loader',
27 | }
28 | },
29 | {
30 | test: /\.ts$/,
31 | use: 'ts-loader',
32 | exclude: /node_modules/,
33 | }
34 | ]
35 | },
36 | optimization: {
37 | minimize: true,
38 | minimizer: [new TerserPlugin()],
39 | },
40 | devServer: {
41 | host: '0.0.0.0',
42 | static: [
43 | path.join(__dirname, 'example'),
44 | ],
45 | },
46 | };
47 |
48 | module.exports = [config];
49 |
--------------------------------------------------------------------------------