Поддержка событий в карточках блога

This commit is contained in:
Иван Кузьменко 2025-11-16 10:55:18 +03:00
parent 17db6d4f3e
commit 9b03d0c197
3 changed files with 101 additions and 30 deletions

View file

@ -10,7 +10,9 @@
import { import {
BLOG_POST_FRESHNESS_MILLIS, BLOG_POST_FRESHNESS_MILLIS,
blogPostTypeToIcon, blogPostTypeToIcon,
blogPostTypeToString blogPostTypeToString,
EventStatus,
postEventStatus
} from '$lib/util/Blogs'; } from '$lib/util/Blogs';
import DateWidget from '$lib/components/DateWidget.svelte'; import DateWidget from '$lib/components/DateWidget.svelte';
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
@ -26,15 +28,20 @@
let isPostNew = $state(false); let isPostNew = $state(false);
let isPostUpdated = $state(false); let isPostUpdated = $state(false);
let isPostFresh = $derived(isPostNew || isPostUpdated); let isPostFresh = $derived(isPostNew || isPostUpdated);
// TODO: rndtrash: события и их актуальность let eventStatus: EventStatus | undefined = $state(undefined);
let isPostEventOfInterest = $derived(
type === 'event' &&
(eventStatus === EventStatus.NotStarted || eventStatus === EventStatus.InProgress)
);
let eventHasStarted = $derived(eventStatus === EventStatus.InProgress);
onMount(() => { onMount(() => {
// rndtrash: Выполняем проверки на клиенте, чтобы плашка не скомпилировалась и потом не потеряла актуальность
const dateNow = new Date().valueOf(); const dateNow = new Date().valueOf();
isPostNew = dateNow - new Date(post.date!).valueOf() <= BLOG_POST_FRESHNESS_MILLIS; isPostNew = dateNow - new Date(post.date!).valueOf() <= BLOG_POST_FRESHNESS_MILLIS;
isPostUpdated = isPostUpdated =
post.dateChanged != null && post.dateChanged != null &&
dateNow - new Date(post.dateChanged).valueOf() <= BLOG_POST_FRESHNESS_MILLIS; dateNow - new Date(post.dateChanged).valueOf() <= BLOG_POST_FRESHNESS_MILLIS;
eventStatus = postEventStatus(post);
}); });
/** /**
@ -71,7 +78,15 @@
href="/blog/{post.slug}" href="/blog/{post.slug}"
class="blog-card class="blog-card
{fullHeight ? 'min-h-full' : ''} {fullHeight ? 'min-h-full' : ''}
{isPostUpdated ? 'updated' : isPostNew ? 'new' : ''} {isPostEventOfInterest
? eventHasStarted
? 'eventOngoing'
: 'eventPreStart'
: isPostUpdated
? 'updated'
: isPostNew
? 'new'
: ''}
{shortClass('flex-col justify-baseline')} {shortClass('flex-col justify-baseline')}
{fullClass('flex-row justify-stretch', 'sm:flex-row sm:justify-stretch')}" {fullClass('flex-row justify-stretch', 'sm:flex-row sm:justify-stretch')}"
> >
@ -87,7 +102,15 @@
alt={post.thumbnailAlt ?? 'Миниатюра поста'} alt={post.thumbnailAlt ?? 'Миниатюра поста'}
/> />
{/if} {/if}
{#if isPostFresh} {#if isPostEventOfInterest}
<div class="toast {fullClass('hidden', 'sm:hidden')}">
{#if eventStatus === EventStatus.NotStarted}
СКОРО НАЧАЛО
{:else}
СОБЫТИЕ
{/if}
</div>
{:else if isPostFresh}
<div class="toast {fullClass('hidden', 'sm:hidden')}"> <div class="toast {fullClass('hidden', 'sm:hidden')}">
{#if isPostUpdated} {#if isPostUpdated}
ОБНОВЛЕНО ОБНОВЛЕНО
@ -99,6 +122,20 @@
</div> </div>
<div class="flex w-full flex-col justify-center p-4 break-words"> <div class="flex w-full flex-col justify-center p-4 break-words">
<div class="flex flex-row flex-wrap justify-start gap-4 pb-2"> <div class="flex flex-row flex-wrap justify-start gap-4 pb-2">
{#if isPostEventOfInterest}
<DateWidget
class={eventHasStarted ? shortClass('hidden', 'not-sm:hidden') : ''}
dateString={post.dateEventFrom}
type="eventStart"
highlight={!eventHasStarted}
/>
<DateWidget
class={!eventHasStarted ? shortClass('hidden', 'not-sm:hidden') : ''}
dateString={post.dateEventTo}
type="eventFinish"
highlight={eventHasStarted}
/>
{:else}
<DateWidget <DateWidget
class={post.dateChanged ? shortClass('hidden', 'not-sm:hidden') : ''} class={post.dateChanged ? shortClass('hidden', 'not-sm:hidden') : ''}
dateString={post.date} dateString={post.date}
@ -118,6 +155,7 @@
{blogPostTypeToString(type)} {blogPostTypeToString(type)}
</span> </span>
</div> </div>
{/if}
</div> </div>
<h2 class="text-3xl font-bold">{post.title}</h2> <h2 class="text-3xl font-bold">{post.title}</h2>
@ -139,7 +177,7 @@
} }
&.updated { &.updated {
box-shadow: 0 0 0 4px var(--color-purple-600); box-shadow: 0 0 0 calc(var(--spacing) * 1) var(--color-purple-600);
.toast { .toast {
@apply bg-purple-600; @apply bg-purple-600;
@ -147,13 +185,29 @@
} }
&.new { &.new {
box-shadow: 0 0 0 4px var(--color-amber-600); box-shadow: 0 0 0 calc(var(--spacing) * 1) var(--color-amber-600);
.toast { .toast {
@apply bg-amber-600; @apply bg-amber-600;
} }
} }
&.eventPreStart {
box-shadow: 0 0 0 calc(var(--spacing) * 1) var(--color-rose-600);
.toast {
@apply bg-rose-600;
}
}
&.eventOngoing {
box-shadow: 0 0 0 calc(var(--spacing) * 1) var(--color-teal-600);
.toast {
@apply bg-teal-600;
}
}
img.thumbnail { img.thumbnail {
@apply absolute h-full w-full object-cover transition-transform; @apply absolute h-full w-full object-cover transition-transform;

View file

@ -10,7 +10,7 @@
class: className = '' class: className = ''
}: { }: {
dateString?: string; dateString?: string;
type?: 'published' | 'updated'; type?: 'published' | 'updated' | 'eventStart' | 'eventFinish';
highlight?: boolean; highlight?: boolean;
showTime?: boolean; showTime?: boolean;
class?: string; class?: string;
@ -18,7 +18,9 @@
const typeToIcon = { const typeToIcon = {
published: 'material-symbols:calendar-today', published: 'material-symbols:calendar-today',
updated: 'material-symbols:update' updated: 'material-symbols:update',
eventStart: 'material-symbols:rocket-launch',
eventFinish: 'material-symbols:sports-score'
}; };
const icon = type ? typeToIcon[type] : undefined; const icon = type ? typeToIcon[type] : undefined;
@ -28,7 +30,12 @@
<div <div
class="flex flex-nowrap items-center gap-2 p-1 text-lg font-bold {className} rounded-lg class="flex flex-nowrap items-center gap-2 p-1 text-lg font-bold {className} rounded-lg
{highlightClasses(type == 'published' ? 'bg-amber-600' : 'bg-purple-600')} {highlightClasses(
type === 'published' ? 'bg-amber-600' : type === 'updated' ? 'bg-purple-600' : ''
)}
{highlightClasses(
type === 'eventStart' ? 'bg-rose-600' : type === 'eventFinish' ? 'bg-teal-600' : ''
)}
{highlightClasses('text-slate-50')}" {highlightClasses('text-slate-50')}"
> >
{#if icon} {#if icon}

View file

@ -8,6 +8,11 @@ export enum EventStatus {
IsOver IsOver
} }
export function postIsEventOfInterest(post: App.BlogPost): boolean {
const status = postEventStatus(post);
return status === EventStatus.NotStarted || status === EventStatus.InProgress;
}
export function postEventStatus(post: App.BlogPost): EventStatus { export function postEventStatus(post: App.BlogPost): EventStatus {
if (post.type !== 'event' || post.dateEventFrom === undefined || post.dateEventTo === undefined) { if (post.type !== 'event' || post.dateEventFrom === undefined || post.dateEventTo === undefined) {
return EventStatus.NotEvent; return EventStatus.NotEvent;
@ -27,12 +32,17 @@ 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(); export const sortPostsByPostDate: PostComparer = (a, b) => new Date(b.date!).valueOf() - new Date(a.date!).valueOf();
function laterDate(a: string, b?: string): number { function laterDate(a: string, ...dates: (string | undefined)[]): number {
const dateA = new Date(a).valueOf(); const dateA = new Date(a).valueOf();
if (!b) return dateA; if (dates.length <= 0) return dateA;
const dateB = new Date(b).valueOf(); let max = dateA;
return Math.max(dateA, dateB); for (const d of dates) {
if (!d) continue;
const date = new Date(d).valueOf();
max = Math.max(max, date);
};
return max;
} }
export const sortPostsByPostAndUpdateDate: PostComparer = (a, b) => laterDate(b.date!, b.dateChanged) - laterDate(a.date!, a.dateChanged); export const sortPostsByPostAndUpdateDate: PostComparer = (a, b) => laterDate(b.date!, b.dateChanged) - laterDate(a.date!, a.dateChanged);