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",
|
||||
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.2.tgz",
|
||||
"integrity": "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==",
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)",
|
||||
"peer": true
|
||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/@citation-js/core": {
|
||||
"version": "0.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.14.tgz",
|
||||
"integrity": "sha512-dgeGqYDSQmn2MtnWZkwPGpJQPh43yr1lAAr9jl1NJ9pIY1RXUQxtlAUZVur0V9PHdbfQC+kkvB1KC3VpgVV3MA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@citation-js/date": "^0.5.0",
|
||||
"@citation-js/name": "^0.4.2",
|
||||
@ -2611,8 +2611,7 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
|
||||
"integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
|
||||
"license": "MIT/X11",
|
||||
"peer": true
|
||||
"license": "MIT/X11"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
@ -2732,8 +2731,7 @@
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
|
||||
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
@ -3106,6 +3104,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@ -3284,6 +3283,7 @@
|
||||
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@ -3696,7 +3696,6 @@
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@ -5854,6 +5853,7 @@
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz",
|
||||
"integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
@ -6415,7 +6415,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@ -6500,7 +6499,6 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"sass": "1.97.2"
|
||||
}
|
||||
@ -6517,7 +6515,6 @@
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6534,7 +6531,6 @@
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6551,7 +6547,6 @@
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6568,7 +6563,6 @@
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6585,7 +6579,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6602,7 +6595,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6619,7 +6611,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6636,7 +6627,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6653,7 +6643,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6670,7 +6659,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6687,7 +6675,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6704,7 +6691,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6721,7 +6707,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6738,7 +6723,6 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6755,7 +6739,6 @@
|
||||
"!linux",
|
||||
"!win32"
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"sass": "1.97.2"
|
||||
}
|
||||
@ -6772,7 +6755,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6789,7 +6771,6 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
@ -6959,6 +6940,7 @@
|
||||
"version": "1.26.2",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-1.26.2.tgz",
|
||||
"integrity": "sha512-iP7u2NA9A6JwRRCkIUREEX2cMhlYV5EBmbbSlfSRvPThwca8HBRbVkWuNWW+kw9+i6BSUZqqG6YeUs5dC2SjZw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@shikijs/core": "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",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
@ -7158,7 +7139,6 @@
|
||||
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
|
||||
"integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"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",
|
||||
"integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
@ -7492,8 +7471,7 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
|
||||
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vfile": {
|
||||
"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 Flex from "./Flex"
|
||||
import ConditionalRender from "./ConditionalRender"
|
||||
import RecentChanges from "./RecentChanges"
|
||||
|
||||
export {
|
||||
ArticleTitle,
|
||||
@ -50,4 +51,5 @@ export {
|
||||
Comments,
|
||||
Flex,
|
||||
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: "آخر الملاحظات",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `مقتبس من ${targetSlug}`,
|
||||
linkToOriginal: "وصلة للملاحظة الرئيسة",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Notes Recents",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Transcluit de ${targetSlug}`,
|
||||
linkToOriginal: "Enllaç a l'original",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Nejnovější poznámky",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Zobrazení ${targetSlug}`,
|
||||
linkToOriginal: "Odkaz na původní dokument",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Zuletzt bearbeitete Seiten",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Transklusion von ${targetSlug}`,
|
||||
linkToOriginal: "Link zum Original",
|
||||
|
||||
@ -48,6 +48,16 @@ export interface Translation {
|
||||
title: string
|
||||
seeRemainingMore: (variables: { remaining: number }) => string
|
||||
}
|
||||
recentChanges: {
|
||||
title: string
|
||||
filterAll: string
|
||||
filterNew: string
|
||||
filterUpdated: string
|
||||
loadMoreTemplate: string
|
||||
noChanges: string
|
||||
badgeNew: string
|
||||
badgeUpdated: string
|
||||
}
|
||||
transcludes: {
|
||||
transcludeOf: (variables: { targetSlug: FullSlug }) => string
|
||||
linkToOriginal: string
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Recent Notes",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
|
||||
linkToOriginal: "Link to original",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Recent Notes",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Transclude of ${targetSlug}`,
|
||||
linkToOriginal: "Link to original",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Notas Recientes",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Transcluido de ${targetSlug}`,
|
||||
linkToOriginal: "Enlace al original",
|
||||
|
||||
@ -46,6 +46,16 @@ export default {
|
||||
title: "یادداشتهای اخیر",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `از ${targetSlug}`,
|
||||
linkToOriginal: "پیوند به اصلی",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Viimeisimmät muistiinpanot",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Upote kohteesta ${targetSlug}`,
|
||||
linkToOriginal: "Linkki alkuperäiseen",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Notes Récentes",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Transclusion de ${targetSlug}`,
|
||||
linkToOriginal: "Lien vers l'original",
|
||||
|
||||
@ -46,6 +46,16 @@ export default {
|
||||
title: "הערות אחרונות",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `מצוטט מ ${targetSlug}`,
|
||||
linkToOriginal: "קישור למקורי",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Legutóbbi jegyzetek",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `${targetSlug} áthivatkozása`,
|
||||
linkToOriginal: "Hivatkozás az eredetire",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Catatan Terbaru",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Transklusi dari ${targetSlug}`,
|
||||
linkToOriginal: "Tautan ke asli",
|
||||
|
||||
@ -46,6 +46,16 @@ export default {
|
||||
seeRemainingMore: ({ 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: {
|
||||
transcludeOf: ({ targetSlug }) => `Inclusione di ${targetSlug}`,
|
||||
linkToOriginal: "Link all'originale",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "最近の記事",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `${targetSlug}のまとめ`,
|
||||
linkToOriginal: "元記事へのリンク",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Соңғы жазбалар",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `${targetSlug} кірістіру`,
|
||||
linkToOriginal: "Бастапқыға сілтеме",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "최근 게시글",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `${targetSlug}의 포함`,
|
||||
linkToOriginal: "원본 링크",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Naujausi Užrašai",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Įterpimas iš ${targetSlug}`,
|
||||
linkToOriginal: "Nuoroda į originalą",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Nylige notater",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Transkludering of ${targetSlug}`,
|
||||
linkToOriginal: "Lenke til original",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Recente notities",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Invoeging van ${targetSlug}`,
|
||||
linkToOriginal: "Link naar origineel",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Najnowsze notatki",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Osadzone ${targetSlug}`,
|
||||
linkToOriginal: "Łącze do oryginału",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Notas recentes",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Transcrever de ${targetSlug}`,
|
||||
linkToOriginal: "Link ao original",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Notițe recente",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Extras din ${targetSlug}`,
|
||||
linkToOriginal: "Legătură către original",
|
||||
|
||||
@ -46,6 +46,16 @@ export default {
|
||||
seeRemainingMore: ({ 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: {
|
||||
transcludeOf: ({ targetSlug }) => `Переход из ${targetSlug}`,
|
||||
linkToOriginal: "Ссылка на оригинал",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "บันทึกล่าสุด",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `รวมข้ามเนื้อหาจาก ${targetSlug}`,
|
||||
linkToOriginal: "ดูหน้าต้นทาง",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Son Notlar",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `${targetSlug} sayfasından alıntı`,
|
||||
linkToOriginal: "Orijinal bağlantı",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Останні нотатки",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Видобуто з ${targetSlug}`,
|
||||
linkToOriginal: "Посилання на оригінал",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "Ghi chú gần đây",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `Trích dẫn toàn bộ từ ${targetSlug}`,
|
||||
linkToOriginal: "Xem trang gốc",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "最近的笔记",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `包含${targetSlug}`,
|
||||
linkToOriginal: "指向原始笔记的链接",
|
||||
|
||||
@ -45,6 +45,16 @@ export default {
|
||||
title: "最近的筆記",
|
||||
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: {
|
||||
transcludeOf: ({ targetSlug }) => `包含 ${targetSlug}`,
|
||||
linkToOriginal: "指向原始筆記的連結",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user