From bc46ae2adc17d6dd7c7e6b60588acffca33a7355 Mon Sep 17 00:00:00 2001 From: Erick Zhao Date: Thu, 1 May 2025 15:10:56 -0700 Subject: [PATCH] feat: use `date-fns` for human-readable time spans --- app/components/ReleaseTable.tsx | 13 ++++-- app/helpers/time.test.ts | 70 +-------------------------------- app/helpers/time.ts | 20 ---------- package-lock.json | 43 ++++++-------------- package.json | 1 + 5 files changed, 25 insertions(+), 122 deletions(-) diff --git a/app/components/ReleaseTable.tsx b/app/components/ReleaseTable.tsx index 7fa48b3..aeef99b 100644 --- a/app/components/ReleaseTable.tsx +++ b/app/components/ReleaseTable.tsx @@ -1,6 +1,7 @@ import { Link } from '@remix-run/react'; +import { formatDistance } from 'date-fns'; import { ElectronRelease } from '~/data/release-data'; -import { humanFriendlyDaysSince, prettyReleaseDate } from '~/helpers/time'; +import { prettyDateString, prettyReleaseDate } from '~/helpers/time'; type ReleaseTableProps = { releases: (ElectronRelease | undefined)[]; @@ -91,10 +92,16 @@ export const ReleaseTable = ({ prefetch="intent" > - {humanFriendlyDaysSince(release)} - + {prettyReleaseDate(release, timeZone)} + + {release.fullDate + ? formatDistance(release.fullDate, Date.now(), { + addSuffix: true, + }) + : ''} + diff --git a/app/helpers/time.test.ts b/app/helpers/time.test.ts index f686e7b..3ccb8f7 100644 --- a/app/helpers/time.test.ts +++ b/app/helpers/time.test.ts @@ -1,73 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { humanFriendlyDaysSince, prettyDateString, prettyReleaseDate } from './time'; - -describe('humanFriendlyDaysSince', () => { - test('should return "Today" for today\'s date', () => { - expect( - humanFriendlyDaysSince( - { fullDate: '2020-02-02T00:00:00Z' }, - new Date('2020-02-02T00:00:00Z'), - ), - ).toBe('Today'); - }); - - test('should return "Yesterday" for yesterday\'s date', () => { - expect( - humanFriendlyDaysSince( - { fullDate: '2020-02-01T00:00:00Z' }, - new Date('2020-02-02T00:00:00Z'), - ), - ).toBe('Yesterday'); - }); - - test('should return "N days ago" for dates in the past', () => { - expect( - humanFriendlyDaysSince( - { fullDate: '2020-01-30T00:00:00Z' }, - new Date('2020-02-02T00:00:00Z'), - ), - ).toBe('3 days ago'); - expect( - humanFriendlyDaysSince( - { fullDate: '2020-01-31T00:00:00Z' }, - new Date('2020-02-02T00:00:00Z'), - ), - ).toBe('2 days ago'); - }); - - test('should return "N days ago" for dates in the past over month boundaries', () => { - expect( - humanFriendlyDaysSince( - { fullDate: '2020-01-30T00:00:00Z' }, - new Date('2020-03-02T00:00:00Z'), - ), - ).toBe('32 days ago'); - expect( - humanFriendlyDaysSince( - { fullDate: '2020-01-31T00:00:00Z' }, - new Date('2020-03-02T00:00:00Z'), - ), - ).toBe('31 days ago'); - }); - - test('should return "Tomorrow" for tomorrow', () => { - expect( - humanFriendlyDaysSince( - { fullDate: '2020-03-04T00:00:00Z' }, - new Date('2020-03-03T00:00:00Z'), - ), - ).toBe('Tomorrow'); - }); - - test('should return "N days in the future" for dates in the future', () => { - expect( - humanFriendlyDaysSince( - { fullDate: '2020-03-04T00:00:00Z' }, - new Date('2020-02-01T00:00:00Z'), - ), - ).toBe('32 days in the future'); - }); -}); +import { prettyDateString, prettyReleaseDate } from './time'; describe('prettyReleaseDate', () => { test('should format date correctly', () => { diff --git a/app/helpers/time.ts b/app/helpers/time.ts index 841ee88..acc3e0a 100644 --- a/app/helpers/time.ts +++ b/app/helpers/time.ts @@ -1,25 +1,5 @@ import { ElectronRelease } from '~/data/release-data'; -/** - * @param date A date string from an ElectronRelease, or any date string in the format - * YYYY-MM-DD - * @returns A human friendly string describing how many days ago the date was - */ -export const humanFriendlyDaysSince = ( - release: Pick, - currentDate = new Date(), -) => { - const releaseDate = new Date(release.fullDate); - const daysAgo = Math.floor( - (currentDate.getTime() - releaseDate.getTime()) / (1_000 * 60 * 60 * 24), - ); - if (daysAgo === 0) return 'Today'; - if (daysAgo === 1) return 'Yesterday'; - if (daysAgo === -1) return 'Tomorrow'; - if (daysAgo < 0) return `${-daysAgo} days in the future`; - return `${daysAgo} days ago`; -}; - /** * This function requires a timezone, do not call it from inside a memoized or cached * data function. Return a date string from those functions and in the loader apply diff --git a/package-lock.json b/package-lock.json index aa79d7d..6b4e219 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "name": "release-v2", + "hasInstallScript": true, "dependencies": { "@electron/github-app-auth": "^3.0.0", "@icons-pack/react-simple-icons": "^12.7.0", @@ -17,12 +18,14 @@ "@remix-run/react": "^2.16.5", "@remix-run/serve": "^2.16.5", "cookie": "^1.0.2", + "date-fns": "^4.1.0", "dompurify": "^3.2.5", "geoip-lite": "^1.4.10", "isbot": "^5.1.26", "jsdom": "^26.1.0", "lucide-react": "^0.503.0", "markdown-it": "^14.1.0", + "patch-package": "^8.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "semver": "^7.7.1" @@ -53,7 +56,6 @@ "husky": "^9.1.7", "jiti": "^2.4.2", "lint-staged": "^15.5.1", - "patch-package": "^8.0.0", "postcss": "^8.4.38", "prettier": "^3.5.3", "tailwindcss": "^3.4.4", @@ -4113,7 +4115,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/@zxing/text-encoding": { @@ -4518,7 +4519,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, "license": "ISC", "engines": { "node": ">= 4.0.0" @@ -5104,7 +5104,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, "funding": [ { "type": "github", @@ -5403,7 +5402,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5418,7 +5416,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -5559,6 +5556,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -7205,7 +7212,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "micromatch": "^4.0.2" @@ -7643,7 +7649,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -8332,7 +8337,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -8681,7 +8685,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, "license": "MIT", "dependencies": { "is-docker": "^2.0.0" @@ -8694,7 +8697,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isbot": { @@ -8710,7 +8712,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -8943,7 +8944,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -8983,7 +8983,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -8996,7 +8995,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", - "dev": true, "license": "Public Domain", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9032,7 +9030,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.1.11" @@ -10753,7 +10750,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -10848,7 +10844,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11346,7 +11341,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11492,7 +11486,6 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "dev": true, "license": "MIT", "dependencies": { "is-docker": "^2.0.0", @@ -11551,7 +11544,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11724,7 +11716,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", - "dev": true, "license": "MIT", "dependencies": { "@yarnpkg/lockfile": "^1.1.0", @@ -11755,7 +11746,6 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", @@ -11790,7 +11780,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13210,7 +13199,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -13223,7 +13211,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13319,7 +13306,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -14254,7 +14240,6 @@ "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, "license": "MIT", "dependencies": { "os-tmpdir": "~1.0.2" @@ -14770,7 +14755,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -16053,7 +16037,6 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 4bc571a..d320262 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@remix-run/react": "^2.16.5", "@remix-run/serve": "^2.16.5", "cookie": "^1.0.2", + "date-fns": "^4.1.0", "dompurify": "^3.2.5", "geoip-lite": "^1.4.10", "isbot": "^5.1.26",