mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-21 21:45:42 -05:00
feat(components): add RecentChanges component
Adds a RecentChanges component that displays a live activity feed of recently created and modified notes, with richer UX than RecentNotes. Features: - Created vs. modified distinction (badge + 1h threshold heuristic) - Tab filter UI: All / New (by creation date) / Updated (modified only) - Load-more pagination per tab with configurable page size - Client-side relative timestamps via Intl.RelativeTimeFormat (locale-aware) - Progressive enhancement: SSR initial render + JSON data island for client - localStorage persistence of the active filter tab - Fully i18n: all UI strings go through cfg.locale New files: - quartz/components/RecentChanges.tsx - quartz/components/scripts/recentChanges.inline.ts - quartz/components/utils/recentChanges.ts - quartz/components/styles/recentChanges.scss Modified: - quartz/components/index.ts: export RecentChanges - quartz/i18n/locales/definition.ts: add recentChanges translation block - quartz/i18n/locales/*.ts (30 files): add English fallback translations
This commit is contained in:
parent
59b5807601
commit
b29dc907e8
40
package-lock.json
generated
40
package-lock.json
generated
@ -97,13 +97,13 @@
|
|||||||
"version": "2.10.2",
|
"version": "2.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.2.tgz",
|
||||||
"integrity": "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==",
|
"integrity": "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==",
|
||||||
"license": "(Apache-2.0 AND BSD-3-Clause)",
|
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@citation-js/core": {
|
"node_modules/@citation-js/core": {
|
||||||
"version": "0.7.14",
|
"version": "0.7.14",
|
||||||
"resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.14.tgz",
|
||||||
"integrity": "sha512-dgeGqYDSQmn2MtnWZkwPGpJQPh43yr1lAAr9jl1NJ9pIY1RXUQxtlAUZVur0V9PHdbfQC+kkvB1KC3VpgVV3MA==",
|
"integrity": "sha512-dgeGqYDSQmn2MtnWZkwPGpJQPh43yr1lAAr9jl1NJ9pIY1RXUQxtlAUZVur0V9PHdbfQC+kkvB1KC3VpgVV3MA==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@citation-js/date": "^0.5.0",
|
"@citation-js/date": "^0.5.0",
|
||||||
"@citation-js/name": "^0.4.2",
|
"@citation-js/name": "^0.4.2",
|
||||||
@ -2611,8 +2611,7 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
|
||||||
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
|
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
|
||||||
"license": "MIT/X11",
|
"license": "MIT/X11"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
@ -2732,8 +2731,7 @@
|
|||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
|
||||||
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/comma-separated-tokens": {
|
"node_modules/comma-separated-tokens": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
@ -3106,6 +3104,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@ -3284,6 +3283,7 @@
|
|||||||
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
@ -3696,7 +3696,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@ -5854,6 +5853,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz",
|
||||||
"integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==",
|
"integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/preact"
|
"url": "https://opencollective.com/preact"
|
||||||
@ -6415,7 +6415,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
@ -6500,7 +6499,6 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sass": "1.97.2"
|
"sass": "1.97.2"
|
||||||
}
|
}
|
||||||
@ -6517,7 +6515,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6534,7 +6531,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6551,7 +6547,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6568,7 +6563,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6585,7 +6579,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6602,7 +6595,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6619,7 +6611,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6636,7 +6627,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6653,7 +6643,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6670,7 +6659,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6687,7 +6675,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6704,7 +6691,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6721,7 +6707,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6738,7 +6723,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6755,7 +6739,6 @@
|
|||||||
"!linux",
|
"!linux",
|
||||||
"!win32"
|
"!win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sass": "1.97.2"
|
"sass": "1.97.2"
|
||||||
}
|
}
|
||||||
@ -6772,7 +6755,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6789,7 +6771,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@ -6959,6 +6940,7 @@
|
|||||||
"version": "1.26.2",
|
"version": "1.26.2",
|
||||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.26.2.tgz",
|
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.26.2.tgz",
|
||||||
"integrity": "sha512-iP7u2NA9A6JwRRCkIUREEX2cMhlYV5EBmbbSlfSRvPThwca8HBRbVkWuNWW+kw9+i6BSUZqqG6YeUs5dC2SjZw==",
|
"integrity": "sha512-iP7u2NA9A6JwRRCkIUREEX2cMhlYV5EBmbbSlfSRvPThwca8HBRbVkWuNWW+kw9+i6BSUZqqG6YeUs5dC2SjZw==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/core": "1.26.2",
|
"@shikijs/core": "1.26.2",
|
||||||
"@shikijs/engine-javascript": "1.26.2",
|
"@shikijs/engine-javascript": "1.26.2",
|
||||||
@ -7130,7 +7112,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
},
|
},
|
||||||
@ -7158,7 +7139,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
|
||||||
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
|
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sync-message-port": "^1.0.0"
|
"sync-message-port": "^1.0.0"
|
||||||
},
|
},
|
||||||
@ -7183,7 +7163,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz",
|
||||||
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
|
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
@ -7492,8 +7471,7 @@
|
|||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
|
||||||
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
|
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/vfile": {
|
"node_modules/vfile": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
|
|||||||
269
quartz/components/RecentChanges.tsx
Normal file
269
quartz/components/RecentChanges.tsx
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
||||||
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
|
import { classNames } from "../util/lang"
|
||||||
|
import { i18n } from "../i18n"
|
||||||
|
import { GlobalConfiguration } from "../cfg"
|
||||||
|
import style from "./styles/recentChanges.scss"
|
||||||
|
import { ChangedItem } from "./utils/recentChanges"
|
||||||
|
// @ts-ignore
|
||||||
|
import script from "./scripts/recentChanges.inline"
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
title?: string
|
||||||
|
limit: number
|
||||||
|
showCreated: boolean
|
||||||
|
showModified: boolean
|
||||||
|
detailed: boolean
|
||||||
|
filterBy: string[]
|
||||||
|
showExcerpt: boolean
|
||||||
|
showTags: boolean
|
||||||
|
showFilter: boolean
|
||||||
|
pageSize: number
|
||||||
|
linkToMore: SimpleSlug | false
|
||||||
|
pages: FullSlug[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOptions = (_cfg: GlobalConfiguration): Options => ({
|
||||||
|
limit: 10,
|
||||||
|
showCreated: true,
|
||||||
|
showModified: true,
|
||||||
|
detailed: false,
|
||||||
|
filterBy: [],
|
||||||
|
showExcerpt: false,
|
||||||
|
showTags: false,
|
||||||
|
showFilter: false,
|
||||||
|
pageSize: 20,
|
||||||
|
linkToMore: false,
|
||||||
|
pages: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatRelativeDate(date: Date, locale: string): string {
|
||||||
|
const diffMs = Date.now() - date.getTime()
|
||||||
|
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" })
|
||||||
|
const abs = (n: number) => Math.abs(n)
|
||||||
|
const s = Math.round(diffMs / 1000)
|
||||||
|
const m = Math.round(diffMs / 60000)
|
||||||
|
const h = Math.round(diffMs / 3600000)
|
||||||
|
const d = Math.round(diffMs / 86400000)
|
||||||
|
const w = Math.round(diffMs / 604800000)
|
||||||
|
const mo = Math.round(diffMs / 2592000000)
|
||||||
|
const y = Math.round(diffMs / 31536000000)
|
||||||
|
if (abs(s) < 60) return rtf.format(-s, "second")
|
||||||
|
if (abs(m) < 60) return rtf.format(-m, "minute")
|
||||||
|
if (abs(h) < 24) return rtf.format(-h, "hour")
|
||||||
|
if (abs(d) < 7) return rtf.format(-d, "day")
|
||||||
|
if (abs(w) < 4) return rtf.format(-w, "week")
|
||||||
|
if (abs(mo) < 12) return rtf.format(-mo, "month")
|
||||||
|
return rtf.format(-y, "year")
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ((userOpts?: Partial<Options>) => {
|
||||||
|
const RecentChanges: QuartzComponent = ({
|
||||||
|
allFiles,
|
||||||
|
fileData,
|
||||||
|
displayClass,
|
||||||
|
cfg,
|
||||||
|
}: QuartzComponentProps) => {
|
||||||
|
const opts = { ...defaultOptions(cfg), ...userOpts }
|
||||||
|
const t = i18n(cfg.locale).components.recentChanges
|
||||||
|
|
||||||
|
// Only render on specified pages (empty = all pages)
|
||||||
|
if (opts.pages.length > 0 && !opts.pages.some((p) => fileData.slug === p)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter files with valid dates
|
||||||
|
const validFiles = allFiles.filter(
|
||||||
|
(file: QuartzPluginData) => file.dates && (file.dates.created || file.dates.modified),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort by most recent date
|
||||||
|
const sortedFiles = validFiles.sort((a: QuartzPluginData, b: QuartzPluginData) => {
|
||||||
|
const dateA = a.dates?.modified || a.dates?.created || new Date(0)
|
||||||
|
const dateB = b.dates?.modified || b.dates?.created || new Date(0)
|
||||||
|
return dateB.getTime() - dateA.getTime()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert to ChangedItems
|
||||||
|
const allItems: ChangedItem[] = sortedFiles.map((file: QuartzPluginData) => {
|
||||||
|
const created = file.dates?.created
|
||||||
|
const modified = file.dates?.modified
|
||||||
|
|
||||||
|
// A note is "Updated" only if its creation date predates the last modification by >1h.
|
||||||
|
const isModified =
|
||||||
|
created !== undefined &&
|
||||||
|
modified !== undefined &&
|
||||||
|
modified.getTime() - created.getTime() > 60 * 60 * 1000
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: file.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title,
|
||||||
|
link: file.slug as FullSlug,
|
||||||
|
date: file.dates?.modified || file.dates?.created || new Date(),
|
||||||
|
createdDate: file.dates?.created ?? file.dates?.modified ?? new Date(),
|
||||||
|
type: isModified ? "modified" : "created",
|
||||||
|
id: `${file.slug}-${isModified ? "modified" : "created"}`,
|
||||||
|
excerpt: file.frontmatter?.description,
|
||||||
|
tags: file.frontmatter?.tags,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply type filters
|
||||||
|
let filtered = allItems
|
||||||
|
if (!opts.showCreated) {
|
||||||
|
filtered = filtered.filter((item) => item.type !== "created")
|
||||||
|
}
|
||||||
|
if (!opts.showModified) {
|
||||||
|
filtered = filtered.filter((item) => item.type !== "modified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply path filters
|
||||||
|
if (opts.filterBy.length > 0) {
|
||||||
|
filtered = filtered.filter((item) => opts.filterBy.some((f) => item.link.includes(f)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = Math.max(0, filtered.length - opts.limit)
|
||||||
|
|
||||||
|
// JSON data island: all items as compact JSON for progressive client-side injection.
|
||||||
|
// Links are pre-resolved server-side since the client cannot call resolveRelative.
|
||||||
|
// The `i` field records each item's index in this array for deduplication tracking.
|
||||||
|
const allItemsJson = JSON.stringify(
|
||||||
|
filtered.map((item, idx) => ({
|
||||||
|
i: idx,
|
||||||
|
t: item.title,
|
||||||
|
l: resolveRelative(fileData.slug!, item.link),
|
||||||
|
d: item.date.getTime(),
|
||||||
|
c: item.createdDate.getTime(),
|
||||||
|
k: item.type,
|
||||||
|
...(opts.showExcerpt && item.excerpt ? { e: item.excerpt } : {}),
|
||||||
|
...(opts.showTags && item.tags?.length ? { g: item.tags } : {}),
|
||||||
|
})),
|
||||||
|
).replace(/<\//g, "<\\/")
|
||||||
|
|
||||||
|
// For showFilter: pre-render pageSize items per type so both "New" and "Updated" tabs
|
||||||
|
// start with visible content. Each item is tagged with its index in filtered (= allData)
|
||||||
|
// via data-idx so the client can correctly initialize its deduplication set.
|
||||||
|
// For !showFilter: flat limit cap, no Load More.
|
||||||
|
const filteredWithIdx = filtered.map((item, idx) => ({ item, idx }))
|
||||||
|
const initialItems = opts.showFilter
|
||||||
|
? [
|
||||||
|
...filteredWithIdx.filter(({ item }) => item.type === "created").slice(0, opts.pageSize),
|
||||||
|
...filteredWithIdx.filter(({ item }) => item.type === "modified").slice(0, opts.pageSize),
|
||||||
|
].sort((a, b) => b.item.date.getTime() - a.item.date.getTime())
|
||||||
|
: filteredWithIdx.slice(0, opts.limit)
|
||||||
|
|
||||||
|
// i18n strings passed to the client-side script via data attributes
|
||||||
|
const i18nData = JSON.stringify({
|
||||||
|
badgeNew: t.badgeNew,
|
||||||
|
badgeUpdated: t.badgeUpdated,
|
||||||
|
noChanges: t.noChanges,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={classNames(displayClass, "recent-changes")}
|
||||||
|
data-page-size={opts.pageSize}
|
||||||
|
data-detailed={opts.detailed ? "1" : "0"}
|
||||||
|
data-show-excerpt={opts.showExcerpt ? "1" : "0"}
|
||||||
|
data-show-tags={opts.showTags ? "1" : "0"}
|
||||||
|
data-locale={cfg.locale}
|
||||||
|
data-i18n={i18nData}
|
||||||
|
data-load-more-tpl={t.loadMoreTemplate}
|
||||||
|
>
|
||||||
|
<h3>
|
||||||
|
{opts.linkToMore ? (
|
||||||
|
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>
|
||||||
|
{opts.title ?? t.title}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
opts.title ?? t.title
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{opts.showFilter && (
|
||||||
|
<div class="recent-changes-filter" role="group" aria-label="Filter changes">
|
||||||
|
<button data-filter="all" class="active">
|
||||||
|
{t.filterAll}
|
||||||
|
</button>
|
||||||
|
<button data-filter="created">{t.filterNew}</button>
|
||||||
|
<button data-filter="modified">{t.filterUpdated}</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{opts.showFilter && <p class="rc-tab-desc" aria-live="polite"></p>}
|
||||||
|
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<p>{t.noChanges}</p>
|
||||||
|
) : (
|
||||||
|
<ul class={`recent-changes-list ${opts.detailed ? "detailed" : "condensed"}`}>
|
||||||
|
{initialItems.map(({ item, idx }) => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
class={`recent-change-item ${item.type}`}
|
||||||
|
data-type={item.type}
|
||||||
|
data-idx={idx}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={resolveRelative(fileData.slug!, item.link)}
|
||||||
|
class="recent-change-link internal"
|
||||||
|
>
|
||||||
|
<span class="recent-change-title">{item.title}</span>
|
||||||
|
</a>
|
||||||
|
<div class="recent-change-meta">
|
||||||
|
<span class="recent-change-type">
|
||||||
|
{item.type === "created" ? t.badgeNew : t.badgeUpdated}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="recent-change-date"
|
||||||
|
data-timestamp={item.date.getTime().toString()}
|
||||||
|
>
|
||||||
|
{formatRelativeDate(item.date, cfg.locale)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{opts.detailed && opts.showExcerpt && item.excerpt && (
|
||||||
|
<p class="recent-change-excerpt">{item.excerpt}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{opts.detailed && opts.showTags && item.tags && item.tags.length > 0 && (
|
||||||
|
<div class="recent-change-tags">
|
||||||
|
{item.tags.map((tag) => (
|
||||||
|
<span key={tag} class="recent-change-tag">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{opts.showFilter && (
|
||||||
|
// @ts-ignore — dangerouslySetInnerHTML on <script> is valid in Preact
|
||||||
|
<script
|
||||||
|
type="application/json"
|
||||||
|
class="rc-items-data"
|
||||||
|
dangerouslySetInnerHTML={{ __html: allItemsJson }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{opts.showFilter && filtered.length > 0 && (
|
||||||
|
<button class="recent-changes-load-more" style="display:none">
|
||||||
|
Load more
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!opts.showFilter && opts.linkToMore && remaining > 0 && (
|
||||||
|
<div class="recent-changes-more">
|
||||||
|
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>View all changes →</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
RecentChanges.css = style
|
||||||
|
RecentChanges.afterDOMLoaded = script
|
||||||
|
return RecentChanges
|
||||||
|
}) satisfies QuartzComponentConstructor
|
||||||
@ -23,6 +23,7 @@ import Breadcrumbs from "./Breadcrumbs"
|
|||||||
import Comments from "./Comments"
|
import Comments from "./Comments"
|
||||||
import Flex from "./Flex"
|
import Flex from "./Flex"
|
||||||
import ConditionalRender from "./ConditionalRender"
|
import ConditionalRender from "./ConditionalRender"
|
||||||
|
import RecentChanges from "./RecentChanges"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ArticleTitle,
|
ArticleTitle,
|
||||||
@ -50,4 +51,5 @@ export {
|
|||||||
Comments,
|
Comments,
|
||||||
Flex,
|
Flex,
|
||||||
ConditionalRender,
|
ConditionalRender,
|
||||||
|
RecentChanges,
|
||||||
}
|
}
|
||||||
|
|||||||
238
quartz/components/scripts/recentChanges.inline.ts
Normal file
238
quartz/components/scripts/recentChanges.inline.ts
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
function formatRelativeDate(date: Date, locale: string): string {
|
||||||
|
const diffMs = Date.now() - date.getTime()
|
||||||
|
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" })
|
||||||
|
const abs = (n: number) => Math.abs(n)
|
||||||
|
const s = Math.round(diffMs / 1000)
|
||||||
|
const m = Math.round(diffMs / 60000)
|
||||||
|
const h = Math.round(diffMs / 3600000)
|
||||||
|
const d = Math.round(diffMs / 86400000)
|
||||||
|
const w = Math.round(diffMs / 604800000)
|
||||||
|
const mo = Math.round(diffMs / 2592000000)
|
||||||
|
const y = Math.round(diffMs / 31536000000)
|
||||||
|
if (abs(s) < 60) return rtf.format(-s, "second")
|
||||||
|
if (abs(m) < 60) return rtf.format(-m, "minute")
|
||||||
|
if (abs(h) < 24) return rtf.format(-h, "hour")
|
||||||
|
if (abs(d) < 7) return rtf.format(-d, "day")
|
||||||
|
if (abs(w) < 4) return rtf.format(-w, "week")
|
||||||
|
if (abs(mo) < 12) return rtf.format(-mo, "month")
|
||||||
|
return rtf.format(-y, "year")
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RcItemJson {
|
||||||
|
i: number
|
||||||
|
t: string
|
||||||
|
l: string
|
||||||
|
d: number // most-recent-activity timestamp
|
||||||
|
c: number // creation date timestamp
|
||||||
|
k: "created" | "modified"
|
||||||
|
e?: string
|
||||||
|
g?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RcI18n {
|
||||||
|
badgeNew: string
|
||||||
|
badgeUpdated: string
|
||||||
|
noChanges: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupRecentChanges() {
|
||||||
|
// Read page locale from <html lang="..."> (set by Quartz from cfg.locale)
|
||||||
|
const pageLocale = document.documentElement.lang || "en-US"
|
||||||
|
|
||||||
|
// Refresh relative dates on all pre-rendered items
|
||||||
|
const dateEls = document.querySelectorAll<HTMLElement>(".recent-change-date[data-timestamp]")
|
||||||
|
dateEls.forEach((el) => {
|
||||||
|
const ts = parseInt(el.dataset.timestamp!, 10)
|
||||||
|
if (!isNaN(ts)) {
|
||||||
|
el.textContent = formatRelativeDate(new Date(ts), pageLocale)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const containers = document.querySelectorAll<HTMLElement>(".recent-changes")
|
||||||
|
containers.forEach((container) => {
|
||||||
|
const filterGroup = container.querySelector<HTMLElement>(".recent-changes-filter")
|
||||||
|
if (!filterGroup) return
|
||||||
|
|
||||||
|
const locale = container.dataset.locale ?? pageLocale
|
||||||
|
const pageSize = parseInt(container.dataset.pageSize ?? "20", 10)
|
||||||
|
const isDetailed = container.dataset.detailed === "1"
|
||||||
|
const showExcerpt = container.dataset.showExcerpt === "1"
|
||||||
|
const showTags = container.dataset.showTags === "1"
|
||||||
|
const loadMoreTpl = container.dataset.loadMoreTpl ?? "Load {count} more · {remaining} remaining"
|
||||||
|
const i18nData: RcI18n = JSON.parse(container.dataset.i18n ?? "{}")
|
||||||
|
|
||||||
|
const list = container.querySelector<HTMLUListElement>(".recent-changes-list")
|
||||||
|
const loadMoreBtn = container.querySelector<HTMLButtonElement>(".recent-changes-load-more")
|
||||||
|
const tabDesc = container.querySelector<HTMLParagraphElement>(".rc-tab-desc")
|
||||||
|
|
||||||
|
if (!list) return
|
||||||
|
const safeList = list
|
||||||
|
|
||||||
|
const dataScript = container.querySelector<HTMLScriptElement>(".rc-items-data")
|
||||||
|
if (!dataScript) return
|
||||||
|
|
||||||
|
let allData: RcItemJson[]
|
||||||
|
try {
|
||||||
|
allData = JSON.parse(dataScript.textContent ?? "[]")
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Three sort views:
|
||||||
|
// "all" → all notes by most recent activity
|
||||||
|
// "created" → ALL notes by creation date (the "New" tab)
|
||||||
|
// "modified" → only modified notes by modification date
|
||||||
|
const sortedArrays: Record<string, RcItemJson[]> = {
|
||||||
|
all: [...allData].sort((a, b) => b.d - a.d),
|
||||||
|
created: [...allData].sort((a, b) => b.c - a.c),
|
||||||
|
modified: allData.filter((x) => x.k === "modified").sort((a, b) => b.d - a.d),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Human-readable descriptions for each tab
|
||||||
|
const tabDescriptions: Record<string, string> = {
|
||||||
|
all: `All ${allData.length} notes · most recent activity first`,
|
||||||
|
created: `All ${allData.length} notes · sorted by when they were added`,
|
||||||
|
modified: `${sortedArrays.modified.length} revised notes · latest changes first`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-tab injection pointer
|
||||||
|
const injectedCount: Record<string, number> = { all: 0, created: 0, modified: 0 }
|
||||||
|
|
||||||
|
let currentFilter = localStorage.getItem("recent-changes-filter") ?? "all"
|
||||||
|
|
||||||
|
function createItemEl(item: RcItemJson, filter: string): HTMLLIElement {
|
||||||
|
const li = document.createElement("li")
|
||||||
|
li.className = `recent-change-item ${item.k}`
|
||||||
|
li.dataset.type = item.k
|
||||||
|
|
||||||
|
const a = document.createElement("a")
|
||||||
|
a.href = item.l
|
||||||
|
a.className = "recent-change-link internal"
|
||||||
|
const titleSpan = document.createElement("span")
|
||||||
|
titleSpan.className = "recent-change-title"
|
||||||
|
titleSpan.textContent = item.t
|
||||||
|
a.appendChild(titleSpan)
|
||||||
|
li.appendChild(a)
|
||||||
|
|
||||||
|
const meta = document.createElement("div")
|
||||||
|
meta.className = "recent-change-meta"
|
||||||
|
|
||||||
|
const typeSpan = document.createElement("span")
|
||||||
|
typeSpan.className = "recent-change-type"
|
||||||
|
typeSpan.textContent =
|
||||||
|
item.k === "created" ? (i18nData.badgeNew ?? "New") : (i18nData.badgeUpdated ?? "Edited")
|
||||||
|
meta.appendChild(typeSpan)
|
||||||
|
|
||||||
|
// Use creation timestamp for the "New" tab, activity timestamp otherwise
|
||||||
|
const ts = filter === "created" ? item.c : item.d
|
||||||
|
const dateSpan = document.createElement("span")
|
||||||
|
dateSpan.className = "recent-change-date"
|
||||||
|
dateSpan.dataset.timestamp = ts.toString()
|
||||||
|
dateSpan.textContent = formatRelativeDate(new Date(ts), locale)
|
||||||
|
meta.appendChild(dateSpan)
|
||||||
|
|
||||||
|
li.appendChild(meta)
|
||||||
|
|
||||||
|
if (isDetailed && showExcerpt && item.e) {
|
||||||
|
const p = document.createElement("p")
|
||||||
|
p.className = "recent-change-excerpt"
|
||||||
|
p.textContent = item.e
|
||||||
|
li.appendChild(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDetailed && showTags && item.g?.length) {
|
||||||
|
const tagsDiv = document.createElement("div")
|
||||||
|
tagsDiv.className = "recent-change-tags"
|
||||||
|
item.g.forEach((tag) => {
|
||||||
|
const tagSpan = document.createElement("span")
|
||||||
|
tagSpan.className = "recent-change-tag"
|
||||||
|
tagSpan.textContent = tag
|
||||||
|
tagsDiv.appendChild(tagSpan)
|
||||||
|
})
|
||||||
|
li.appendChild(tagsDiv)
|
||||||
|
}
|
||||||
|
|
||||||
|
return li
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTabDesc(filter: string) {
|
||||||
|
if (tabDesc) tabDesc.textContent = tabDescriptions[filter] ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLoadMoreBtn(filter: string) {
|
||||||
|
if (!loadMoreBtn) return
|
||||||
|
const arr = sortedArrays[filter] ?? []
|
||||||
|
const loaded = injectedCount[filter]
|
||||||
|
const remaining = arr.length - loaded
|
||||||
|
if (remaining <= 0) {
|
||||||
|
loadMoreBtn.style.display = "none"
|
||||||
|
} else {
|
||||||
|
const count = Math.min(pageSize, remaining)
|
||||||
|
loadMoreBtn.textContent = loadMoreTpl
|
||||||
|
.replace("{count}", String(count))
|
||||||
|
.replace("{remaining}", String(remaining))
|
||||||
|
loadMoreBtn.style.display = "block"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTab(filter: string) {
|
||||||
|
safeList.innerHTML = ""
|
||||||
|
injectedCount[filter] = 0
|
||||||
|
const arr = sortedArrays[filter] ?? []
|
||||||
|
const end = Math.min(pageSize, arr.length)
|
||||||
|
for (let i = 0; i < end; i++) {
|
||||||
|
safeList.appendChild(createItemEl(arr[i], filter))
|
||||||
|
}
|
||||||
|
injectedCount[filter] = end
|
||||||
|
updateTabDesc(filter)
|
||||||
|
updateLoadMoreBtn(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMore(filter: string) {
|
||||||
|
const arr = sortedArrays[filter] ?? []
|
||||||
|
const start = injectedCount[filter]
|
||||||
|
const end = Math.min(start + pageSize, arr.length)
|
||||||
|
for (let i = start; i < end; i++) {
|
||||||
|
safeList.appendChild(createItemEl(arr[i], filter))
|
||||||
|
}
|
||||||
|
injectedCount[filter] = end
|
||||||
|
updateLoadMoreBtn(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = filterGroup.querySelectorAll<HTMLButtonElement>("button[data-filter]")
|
||||||
|
buttons.forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
currentFilter = btn.dataset.filter ?? "all"
|
||||||
|
localStorage.setItem("recent-changes-filter", currentFilter)
|
||||||
|
buttons.forEach((b) => b.classList.toggle("active", b === btn))
|
||||||
|
renderTab(currentFilter)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loadMoreBtn) {
|
||||||
|
loadMoreBtn.addEventListener("click", () => loadMore(currentFilter))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore saved filter button active state
|
||||||
|
if (currentFilter !== "all") {
|
||||||
|
const savedBtn = filterGroup.querySelector<HTMLButtonElement>(
|
||||||
|
`button[data-filter="${currentFilter}"]`,
|
||||||
|
)
|
||||||
|
if (savedBtn) {
|
||||||
|
buttons.forEach((b) => b.classList.remove("active"))
|
||||||
|
savedBtn.classList.add("active")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize: keep SSR items if on "all" tab, otherwise rebuild
|
||||||
|
if (currentFilter === "all") {
|
||||||
|
injectedCount.all = safeList.querySelectorAll(".recent-change-item").length
|
||||||
|
updateTabDesc("all")
|
||||||
|
updateLoadMoreBtn("all")
|
||||||
|
} else {
|
||||||
|
renderTab(currentFilter)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("nav", setupRecentChanges)
|
||||||
214
quartz/components/styles/recentChanges.scss
Normal file
214
quartz/components/styles/recentChanges.scss
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
.recent-changes {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
|
||||||
|
// Badge colors (customizable via CSS custom properties)
|
||||||
|
--rc-created-bg: #b3e6cc;
|
||||||
|
--rc-created-text: #005500;
|
||||||
|
--rc-modified-bg: #d0e0f0;
|
||||||
|
--rc-modified-text: #003366;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-changes-filter {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
border: 1px solid var(--lightgray);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--darkgray);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s,
|
||||||
|
color 0.15s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--secondary);
|
||||||
|
color: var(--light);
|
||||||
|
border-color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.active) {
|
||||||
|
background: var(--lightgray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-tab-desc {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--gray);
|
||||||
|
margin: -0.5rem 0 0.75rem;
|
||||||
|
min-height: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rc-hidden-filter,
|
||||||
|
.rc-hidden-page {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-changes-load-more {
|
||||||
|
display: block;
|
||||||
|
margin: 1rem auto;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
border: 1px solid var(--lightgray);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--secondary);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--lightgray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-changes-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&.detailed {
|
||||||
|
.recent-change-item {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1.2rem;
|
||||||
|
border-bottom: 1px solid var(--lightgray);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-change-excerpt {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--gray);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-change-tags {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
|
||||||
|
.recent-change-tag {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
background-color: var(--lightgray);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--darkgray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.condensed {
|
||||||
|
.recent-change-item {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-change-item {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.created {
|
||||||
|
.recent-change-type {
|
||||||
|
background-color: var(--rc-created-bg);
|
||||||
|
color: var(--rc-created-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.modified {
|
||||||
|
.recent-change-type {
|
||||||
|
background-color: var(--rc-modified-bg);
|
||||||
|
color: var(--rc-modified-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-change-link {
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--dark);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-change-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.condensed & {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-change-type {
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-change-date {
|
||||||
|
color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-changes-more {
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--secondary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode adjustments
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.recent-changes {
|
||||||
|
--rc-created-bg: rgba(179, 230, 204, 0.2);
|
||||||
|
--rc-created-text: #b3e6cc;
|
||||||
|
--rc-modified-bg: rgba(208, 224, 240, 0.2);
|
||||||
|
--rc-modified-text: #d0e0f0;
|
||||||
|
|
||||||
|
.recent-changes-list.detailed .recent-change-tags .recent-change-tag {
|
||||||
|
background-color: var(--darkgray);
|
||||||
|
color: var(--light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
quartz/components/utils/recentChanges.ts
Normal file
12
quartz/components/utils/recentChanges.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { FullSlug } from "../../util/path"
|
||||||
|
|
||||||
|
export interface ChangedItem {
|
||||||
|
title: string
|
||||||
|
link: FullSlug
|
||||||
|
date: Date // most recent activity (used for "All" and "Updated" sort)
|
||||||
|
createdDate: Date // first known date (used for "New" sort)
|
||||||
|
type: "created" | "modified" // display badge only — not a filter
|
||||||
|
excerpt?: string
|
||||||
|
tags?: string[]
|
||||||
|
id: string
|
||||||
|
}
|
||||||
@ -46,6 +46,16 @@ export default {
|
|||||||
title: "آخر الملاحظات",
|
title: "آخر الملاحظات",
|
||||||
seeRemainingMore: ({ remaining }) => `تصفح ${remaining} أكثر →`,
|
seeRemainingMore: ({ remaining }) => `تصفح ${remaining} أكثر →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `مقتبس من ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `مقتبس من ${targetSlug}`,
|
||||||
linkToOriginal: "وصلة للملاحظة الرئيسة",
|
linkToOriginal: "وصلة للملاحظة الرئيسة",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Notes Recents",
|
title: "Notes Recents",
|
||||||
seeRemainingMore: ({ remaining }) => `Vegi ${remaining} més →`,
|
seeRemainingMore: ({ remaining }) => `Vegi ${remaining} més →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Transcluit de ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Transcluit de ${targetSlug}`,
|
||||||
linkToOriginal: "Enllaç a l'original",
|
linkToOriginal: "Enllaç a l'original",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Nejnovější poznámky",
|
title: "Nejnovější poznámky",
|
||||||
seeRemainingMore: ({ remaining }) => `Zobraz ${remaining} dalších →`,
|
seeRemainingMore: ({ remaining }) => `Zobraz ${remaining} dalších →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Zobrazení ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Zobrazení ${targetSlug}`,
|
||||||
linkToOriginal: "Odkaz na původní dokument",
|
linkToOriginal: "Odkaz na původní dokument",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Zuletzt bearbeitete Seiten",
|
title: "Zuletzt bearbeitete Seiten",
|
||||||
seeRemainingMore: ({ remaining }) => `${remaining} weitere ansehen →`,
|
seeRemainingMore: ({ remaining }) => `${remaining} weitere ansehen →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Transklusion von ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Transklusion von ${targetSlug}`,
|
||||||
linkToOriginal: "Link zum Original",
|
linkToOriginal: "Link zum Original",
|
||||||
|
|||||||
@ -48,6 +48,16 @@ export interface Translation {
|
|||||||
title: string
|
title: string
|
||||||
seeRemainingMore: (variables: { remaining: number }) => string
|
seeRemainingMore: (variables: { remaining: number }) => string
|
||||||
}
|
}
|
||||||
|
recentChanges: {
|
||||||
|
title: string
|
||||||
|
filterAll: string
|
||||||
|
filterNew: string
|
||||||
|
filterUpdated: string
|
||||||
|
loadMoreTemplate: string
|
||||||
|
noChanges: string
|
||||||
|
badgeNew: string
|
||||||
|
badgeUpdated: string
|
||||||
|
}
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: (variables: { targetSlug: FullSlug }) => string
|
transcludeOf: (variables: { targetSlug: FullSlug }) => string
|
||||||
linkToOriginal: string
|
linkToOriginal: string
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Recent Notes",
|
title: "Recent Notes",
|
||||||
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`,
|
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
|
||||||
linkToOriginal: "Link to original",
|
linkToOriginal: "Link to original",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Recent Notes",
|
title: "Recent Notes",
|
||||||
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`,
|
seeRemainingMore: ({ remaining }) => `See ${remaining} more →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
|
||||||
linkToOriginal: "Link to original",
|
linkToOriginal: "Link to original",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Notas Recientes",
|
title: "Notas Recientes",
|
||||||
seeRemainingMore: ({ remaining }) => `Vea ${remaining} más →`,
|
seeRemainingMore: ({ remaining }) => `Vea ${remaining} más →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Transcluido de ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Transcluido de ${targetSlug}`,
|
||||||
linkToOriginal: "Enlace al original",
|
linkToOriginal: "Enlace al original",
|
||||||
|
|||||||
@ -46,6 +46,16 @@ export default {
|
|||||||
title: "یادداشتهای اخیر",
|
title: "یادداشتهای اخیر",
|
||||||
seeRemainingMore: ({ remaining }) => `${remaining} یادداشت دیگر →`,
|
seeRemainingMore: ({ remaining }) => `${remaining} یادداشت دیگر →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `از ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `از ${targetSlug}`,
|
||||||
linkToOriginal: "پیوند به اصلی",
|
linkToOriginal: "پیوند به اصلی",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Viimeisimmät muistiinpanot",
|
title: "Viimeisimmät muistiinpanot",
|
||||||
seeRemainingMore: ({ remaining }) => `Näytä ${remaining} lisää →`,
|
seeRemainingMore: ({ remaining }) => `Näytä ${remaining} lisää →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Upote kohteesta ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Upote kohteesta ${targetSlug}`,
|
||||||
linkToOriginal: "Linkki alkuperäiseen",
|
linkToOriginal: "Linkki alkuperäiseen",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Notes Récentes",
|
title: "Notes Récentes",
|
||||||
seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`,
|
seeRemainingMore: ({ remaining }) => `Voir ${remaining} de plus →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`,
|
||||||
linkToOriginal: "Lien vers l'original",
|
linkToOriginal: "Lien vers l'original",
|
||||||
|
|||||||
@ -46,6 +46,16 @@ export default {
|
|||||||
title: "הערות אחרונות",
|
title: "הערות אחרונות",
|
||||||
seeRemainingMore: ({ remaining }) => `עיין ב ${remaining} נוספים →`,
|
seeRemainingMore: ({ remaining }) => `עיין ב ${remaining} נוספים →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `מצוטט מ ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `מצוטט מ ${targetSlug}`,
|
||||||
linkToOriginal: "קישור למקורי",
|
linkToOriginal: "קישור למקורי",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Legutóbbi jegyzetek",
|
title: "Legutóbbi jegyzetek",
|
||||||
seeRemainingMore: ({ remaining }) => `${remaining} további megtekintése →`,
|
seeRemainingMore: ({ remaining }) => `${remaining} további megtekintése →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `${targetSlug} áthivatkozása`,
|
transcludeOf: ({ targetSlug }) => `${targetSlug} áthivatkozása`,
|
||||||
linkToOriginal: "Hivatkozás az eredetire",
|
linkToOriginal: "Hivatkozás az eredetire",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Catatan Terbaru",
|
title: "Catatan Terbaru",
|
||||||
seeRemainingMore: ({ remaining }) => `Lihat ${remaining} lagi →`,
|
seeRemainingMore: ({ remaining }) => `Lihat ${remaining} lagi →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Transklusi dari ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Transklusi dari ${targetSlug}`,
|
||||||
linkToOriginal: "Tautan ke asli",
|
linkToOriginal: "Tautan ke asli",
|
||||||
|
|||||||
@ -46,6 +46,16 @@ export default {
|
|||||||
seeRemainingMore: ({ remaining }) =>
|
seeRemainingMore: ({ remaining }) =>
|
||||||
remaining === 1 ? "Vedi 1 altra →" : `Vedi altre ${remaining} →`,
|
remaining === 1 ? "Vedi 1 altra →" : `Vedi altre ${remaining} →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Inclusione di ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Inclusione di ${targetSlug}`,
|
||||||
linkToOriginal: "Link all'originale",
|
linkToOriginal: "Link all'originale",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "最近の記事",
|
title: "最近の記事",
|
||||||
seeRemainingMore: ({ remaining }) => `さらに${remaining}件 →`,
|
seeRemainingMore: ({ remaining }) => `さらに${remaining}件 →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `${targetSlug}のまとめ`,
|
transcludeOf: ({ targetSlug }) => `${targetSlug}のまとめ`,
|
||||||
linkToOriginal: "元記事へのリンク",
|
linkToOriginal: "元記事へのリンク",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Соңғы жазбалар",
|
title: "Соңғы жазбалар",
|
||||||
seeRemainingMore: ({ remaining }) => `Тағы ${remaining} жазбаны қарау →`,
|
seeRemainingMore: ({ remaining }) => `Тағы ${remaining} жазбаны қарау →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `${targetSlug} кірістіру`,
|
transcludeOf: ({ targetSlug }) => `${targetSlug} кірістіру`,
|
||||||
linkToOriginal: "Бастапқыға сілтеме",
|
linkToOriginal: "Бастапқыға сілтеме",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "최근 게시글",
|
title: "최근 게시글",
|
||||||
seeRemainingMore: ({ remaining }) => `${remaining}건 더보기 →`,
|
seeRemainingMore: ({ remaining }) => `${remaining}건 더보기 →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `${targetSlug}의 포함`,
|
transcludeOf: ({ targetSlug }) => `${targetSlug}의 포함`,
|
||||||
linkToOriginal: "원본 링크",
|
linkToOriginal: "원본 링크",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Naujausi Užrašai",
|
title: "Naujausi Užrašai",
|
||||||
seeRemainingMore: ({ remaining }) => `Peržiūrėti dar ${remaining} →`,
|
seeRemainingMore: ({ remaining }) => `Peržiūrėti dar ${remaining} →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Įterpimas iš ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Įterpimas iš ${targetSlug}`,
|
||||||
linkToOriginal: "Nuoroda į originalą",
|
linkToOriginal: "Nuoroda į originalą",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Nylige notater",
|
title: "Nylige notater",
|
||||||
seeRemainingMore: ({ remaining }) => `Se ${remaining} til →`,
|
seeRemainingMore: ({ remaining }) => `Se ${remaining} til →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Transkludering of ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Transkludering of ${targetSlug}`,
|
||||||
linkToOriginal: "Lenke til original",
|
linkToOriginal: "Lenke til original",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Recente notities",
|
title: "Recente notities",
|
||||||
seeRemainingMore: ({ remaining }) => `Zie ${remaining} meer →`,
|
seeRemainingMore: ({ remaining }) => `Zie ${remaining} meer →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Invoeging van ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Invoeging van ${targetSlug}`,
|
||||||
linkToOriginal: "Link naar origineel",
|
linkToOriginal: "Link naar origineel",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Najnowsze notatki",
|
title: "Najnowsze notatki",
|
||||||
seeRemainingMore: ({ remaining }) => `Zobacz ${remaining} nastepnych →`,
|
seeRemainingMore: ({ remaining }) => `Zobacz ${remaining} nastepnych →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Osadzone ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Osadzone ${targetSlug}`,
|
||||||
linkToOriginal: "Łącze do oryginału",
|
linkToOriginal: "Łącze do oryginału",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Notas recentes",
|
title: "Notas recentes",
|
||||||
seeRemainingMore: ({ remaining }) => `Veja mais ${remaining} →`,
|
seeRemainingMore: ({ remaining }) => `Veja mais ${remaining} →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Transcrever de ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Transcrever de ${targetSlug}`,
|
||||||
linkToOriginal: "Link ao original",
|
linkToOriginal: "Link ao original",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Notițe recente",
|
title: "Notițe recente",
|
||||||
seeRemainingMore: ({ remaining }) => `Vezi încă ${remaining} →`,
|
seeRemainingMore: ({ remaining }) => `Vezi încă ${remaining} →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Extras din ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Extras din ${targetSlug}`,
|
||||||
linkToOriginal: "Legătură către original",
|
linkToOriginal: "Legătură către original",
|
||||||
|
|||||||
@ -46,6 +46,16 @@ export default {
|
|||||||
seeRemainingMore: ({ remaining }) =>
|
seeRemainingMore: ({ remaining }) =>
|
||||||
`Посмотреть оставш${getForm(remaining, "уюся", "иеся", "иеся")} ${remaining} →`,
|
`Посмотреть оставш${getForm(remaining, "уюся", "иеся", "иеся")} ${remaining} →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Переход из ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Переход из ${targetSlug}`,
|
||||||
linkToOriginal: "Ссылка на оригинал",
|
linkToOriginal: "Ссылка на оригинал",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "บันทึกล่าสุด",
|
title: "บันทึกล่าสุด",
|
||||||
seeRemainingMore: ({ remaining }) => `ดูเพิ่มอีก ${remaining} รายการ →`,
|
seeRemainingMore: ({ remaining }) => `ดูเพิ่มอีก ${remaining} รายการ →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `รวมข้ามเนื้อหาจาก ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `รวมข้ามเนื้อหาจาก ${targetSlug}`,
|
||||||
linkToOriginal: "ดูหน้าต้นทาง",
|
linkToOriginal: "ดูหน้าต้นทาง",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Son Notlar",
|
title: "Son Notlar",
|
||||||
seeRemainingMore: ({ remaining }) => `${remaining} tane daha gör →`,
|
seeRemainingMore: ({ remaining }) => `${remaining} tane daha gör →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `${targetSlug} sayfasından alıntı`,
|
transcludeOf: ({ targetSlug }) => `${targetSlug} sayfasından alıntı`,
|
||||||
linkToOriginal: "Orijinal bağlantı",
|
linkToOriginal: "Orijinal bağlantı",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Останні нотатки",
|
title: "Останні нотатки",
|
||||||
seeRemainingMore: ({ remaining }) => `Переглянути ще ${remaining} →`,
|
seeRemainingMore: ({ remaining }) => `Переглянути ще ${remaining} →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Видобуто з ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Видобуто з ${targetSlug}`,
|
||||||
linkToOriginal: "Посилання на оригінал",
|
linkToOriginal: "Посилання на оригінал",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "Ghi chú gần đây",
|
title: "Ghi chú gần đây",
|
||||||
seeRemainingMore: ({ remaining }) => `Xem thêm ${remaining} ghi chú →`,
|
seeRemainingMore: ({ remaining }) => `Xem thêm ${remaining} ghi chú →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `Trích dẫn toàn bộ từ ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `Trích dẫn toàn bộ từ ${targetSlug}`,
|
||||||
linkToOriginal: "Xem trang gốc",
|
linkToOriginal: "Xem trang gốc",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "最近的笔记",
|
title: "最近的笔记",
|
||||||
seeRemainingMore: ({ remaining }) => `查看更多${remaining}篇笔记 →`,
|
seeRemainingMore: ({ remaining }) => `查看更多${remaining}篇笔记 →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `包含${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `包含${targetSlug}`,
|
||||||
linkToOriginal: "指向原始笔记的链接",
|
linkToOriginal: "指向原始笔记的链接",
|
||||||
|
|||||||
@ -45,6 +45,16 @@ export default {
|
|||||||
title: "最近的筆記",
|
title: "最近的筆記",
|
||||||
seeRemainingMore: ({ remaining }) => `查看更多 ${remaining} 篇筆記 →`,
|
seeRemainingMore: ({ remaining }) => `查看更多 ${remaining} 篇筆記 →`,
|
||||||
},
|
},
|
||||||
|
recentChanges: {
|
||||||
|
title: "Recent Changes",
|
||||||
|
filterAll: "All",
|
||||||
|
filterNew: "New",
|
||||||
|
filterUpdated: "Updated",
|
||||||
|
loadMoreTemplate: "Load {count} more · {remaining} remaining",
|
||||||
|
noChanges: "No recent changes found.",
|
||||||
|
badgeNew: "New",
|
||||||
|
badgeUpdated: "Edited",
|
||||||
|
},
|
||||||
transcludes: {
|
transcludes: {
|
||||||
transcludeOf: ({ targetSlug }) => `包含 ${targetSlug}`,
|
transcludeOf: ({ targetSlug }) => `包含 ${targetSlug}`,
|
||||||
linkToOriginal: "指向原始筆記的連結",
|
linkToOriginal: "指向原始筆記的連結",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user