├── .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 | ![npm](https://img.shields.io/npm/v/@date-js/clever-date?style=flat-square) 4 | ![npm](https://img.shields.io/npm/dt/@date-js/clever-date?style=flat-square) 5 | ![starts](https://img.shields.io/github/stars/date-js/clever-date?style=flat-square) 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 | 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 | --------------------------------------------------------------------------------