From a6bccea8221e53ce85aa880b020adb617539f414 Mon Sep 17 00:00:00 2001 From: Ivan Kuzmenko <6745157+rndtrash@users.noreply.github.com> Date: Sun, 16 Nov 2025 06:24:33 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=A5=D0=B5=D0=BB=D0=BF=D0=B5=D1=80=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20?= =?UTF-8?q?=D1=81=20=D0=B4=D0=B0=D1=82=D0=B0=D0=BC=D0=B8=20+=20polyfill=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B2=D0=B5=D0=B6=D0=B5=D0=B3=D0=BE?= =?UTF-8?q?=20API=202025=20=D0=B3=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 4 +-- package-lock.json | 51 +++++++++++++++++++++++++++- package.json | 5 ++- src/app.d.ts | 13 +++++++ src/lib/components/DateWidget.svelte | 36 +++++++++++++------- src/lib/util/Dates.ts | 42 +++++++++++++++++++++++ 6 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 src/lib/util/Dates.ts diff --git a/.eslintrc.js b/.eslintrc.js index 477d7a8..1c34b9a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,12 +10,12 @@ module.exports = { plugins: ['@typescript-eslint'], parserOptions: { sourceType: 'module', - ecmaVersion: 2020, + ecmaVersion: 2024, extraFileExtensions: ['.svelte'] }, env: { browser: true, - es2017: true, + es2024: true, node: true }, rules: { diff --git a/package-lock.json b/package-lock.json index 5438b0e..6a6592f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "teasanctuary-ru", "version": "0.0.1", + "dependencies": { + "@formatjs/intl-durationformat": "^0.7.6" + }, "devDependencies": { "@iconify/svelte": "^4.2.0", "@react2svelte/swipeable": "^0.1.4", @@ -619,6 +622,47 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-durationformat": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.7.6.tgz", + "integrity": "sha512-jatAN3E84X6aP2UOGK1jTrwD1a7BiG3qWUSEDAhtyNd1BgYeS5wQPtXlnuGF1QRx0DjnwwNOIssyd7oQoRlQeg==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/intl-localematcher": "0.6.2", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1960,6 +2004,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4097,7 +4147,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { diff --git a/package.json b/package.json index ee4f7bc..b211b22 100644 --- a/package.json +++ b/package.json @@ -41,5 +41,8 @@ "typescript": "^5.7.3", "vite": "^6.2.0" }, - "type": "module" + "type": "module", + "dependencies": { + "@formatjs/intl-durationformat": "^0.7.6" + } } diff --git a/src/app.d.ts b/src/app.d.ts index db32518..520d030 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,4 +1,17 @@ +import type { + DurationFormatConstructor, + DurationFormatOptions as _DurationFormatOptions, + DurationInput as _DurationInput, +} from '@formatjs/intl-durationformat/src/types'; + declare global { + // rndtrash: Терпим. https://github.com/microsoft/TypeScript/issues/60608 + namespace Intl { + const DurationFormat: DurationFormatConstructor; + type DurationFormatOptions = _DurationFormatOptions; + type DurationInput = _DurationInput; + } + namespace App { interface Route { label: string; diff --git a/src/lib/components/DateWidget.svelte b/src/lib/components/DateWidget.svelte index 69ab90d..fd52c69 100644 --- a/src/lib/components/DateWidget.svelte +++ b/src/lib/components/DateWidget.svelte @@ -1,15 +1,27 @@ @@ -19,14 +31,12 @@ {highlightClasses(type == 'published' ? 'bg-amber-600' : 'bg-purple-600')} {highlightClasses('text-slate-50')}" > - + {#if icon} + + {/if} {dateString - ? new Date(dateString).toLocaleString(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric' - }) + ? (showTime ? dateFormatLong : dateFormatShort).format(new Date(dateString)) : 'Не опубликован!'} diff --git a/src/lib/util/Dates.ts b/src/lib/util/Dates.ts new file mode 100644 index 0000000..bcf9e71 --- /dev/null +++ b/src/lib/util/Dates.ts @@ -0,0 +1,42 @@ +import { shouldPolyfill } from '@formatjs/intl-durationformat/should-polyfill' +if (shouldPolyfill()) { + import('@formatjs/intl-durationformat/polyfill-force'); +} + +export const dateFormatShort = new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric' +}); + +export const dateFormatLong = new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' +}); + +export const dateDurationLong = new Intl.DurationFormat(undefined, { + style: "long" +}); + +export const dateDurationLongBackup = new Intl.DurationFormat(undefined, { + style: "long", + seconds: "long" +}); + +export const durationHumanReadable = (a: Date, b: Date) => durationHumanReadableMillis(b.valueOf() - a.valueOf()); + +export function durationHumanReadableMillis(dur: number): string { + dur = Math.max(dur, 0); + const seconds = dur / 1000; + const minutes = dur / (1000 * 60); + const hours = dur / (1000 * 60 * 60); + const days = dur / (1000 * 60 * 60 * 24); + + const formatObject = { days: Math.floor(days), hours: Math.floor(hours) % 24, minutes: Math.floor(minutes) % 60, seconds: Math.floor(seconds) % 60 }; + const result = dateDurationLong.format(formatObject); + // Если на выходе получаем пустую строку, то хоть выведем 0 секунд + return result === '' ? dateDurationLongBackup.format(formatObject) : result; +} From 703695ffe54724e2c362afe15ccefffc84a6090d Mon Sep 17 00:00:00 2001 From: Ivan Kuzmenko <6745157+rndtrash@users.noreply.github.com> Date: Sun, 16 Nov 2025 06:28:51 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=9F=D0=B0=D0=BD=D0=B5=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=20=D0=BD=D0=B0=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D0=B5=20=D0=B1=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/CountdownClock.svelte | 99 ++++++++++++++++++++++++ src/lib/util/Blogs.ts | 22 ++++++ src/routes/blog/[slug]/+page.svelte | 84 ++++++++++++++++++-- 3 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 src/lib/components/CountdownClock.svelte diff --git a/src/lib/components/CountdownClock.svelte b/src/lib/components/CountdownClock.svelte new file mode 100644 index 0000000..71c95c9 --- /dev/null +++ b/src/lib/components/CountdownClock.svelte @@ -0,0 +1,99 @@ + + +
+ {#if daysLeft > 0} +
{dateDurationLong.format({ days: daysLeft })}
+ {/if} +
+
{hoursLeft[0]}
+
{hoursLeft[1]}
+
:
+
{minutesLeft[0]}
+
{minutesLeft[1]}
+
:
+
{secondsLeft[0]}
+
{secondsLeft[1]}
+
+
+ + diff --git a/src/lib/util/Blogs.ts b/src/lib/util/Blogs.ts index 5878e26..e10d18d 100644 --- a/src/lib/util/Blogs.ts +++ b/src/lib/util/Blogs.ts @@ -1,6 +1,28 @@ export const THUMBNAIL_DEFAULT = "https://teasanctuary.ru/common/background-day.webp"; export const BLOG_POST_FRESHNESS_MILLIS = 3 * 24 * 60 * 60 * 1000; // 3 дня +export enum EventStatus { + NotEvent = 0, + NotStarted, + InProgress, + IsOver +} + +export function postEventStatus(post: App.BlogPost): EventStatus { + if (post.type !== 'event' || post.dateEventFrom === undefined || post.dateEventTo === undefined) { + return EventStatus.NotEvent; + } + + const currentTime = new Date().valueOf(); + const eventStart = new Date(post.dateEventFrom).valueOf(); + if (currentTime < eventStart) return EventStatus.NotStarted; + + const eventEnd = new Date(post.dateEventTo).valueOf(); + if (currentTime < eventEnd) return EventStatus.InProgress; + + return EventStatus.IsOver; +} + export type PostComparer = (a: App.BlogPost, b: App.BlogPost) => number; export const sortPostsByPostDate: PostComparer = (a, b) => new Date(b.date!).valueOf() - new Date(a.date!).valueOf(); diff --git a/src/routes/blog/[slug]/+page.svelte b/src/routes/blog/[slug]/+page.svelte index 0b0b105..b891e4d 100644 --- a/src/routes/blog/[slug]/+page.svelte +++ b/src/routes/blog/[slug]/+page.svelte @@ -1,12 +1,20 @@ @@ -63,6 +93,50 @@ {/each} +{#if type === 'event'} +
+ {#if eventStatus === EventStatus.NotStarted || eventStatus === EventStatus.InProgress} + + ДО {eventStatus === EventStatus.NotStarted ? 'НАЧАЛА' : 'КОНЦА'} ОСТАЛОСЬ + + + {:else if eventStatus === EventStatus.IsOver} + СОБЫТИЕ ЗАВЕРШЕНО + {/if} + + Событие {eventStatus === EventStatus.IsOver ? 'проводилось' : 'проводится'} с + + по + + ({durationHumanReadable( + new Date(data.blogPost.dateEventFrom!), + new Date(data.blogPost.dateEventTo!) + )}) + +
+{/if} + {#if page.data.blogPost.projects?.length > 0}

В данной заметке упоминаются наши проекты:

@@ -93,4 +167,4 @@ sm:text-xl lg:p-8" > - \ No newline at end of file +