Compare commits
4 commits
e61e48fb51
...
828cba41bd
| Author | SHA1 | Date | |
|---|---|---|---|
| 828cba41bd | |||
| d6c3c4990c | |||
| 9b03d0c197 | |||
| 17db6d4f3e |
8 changed files with 163 additions and 46 deletions
|
|
@ -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';
|
||||||
|
|
@ -20,21 +22,26 @@
|
||||||
post,
|
post,
|
||||||
size = BlogCardSize.Both,
|
size = BlogCardSize.Both,
|
||||||
fullHeight = false
|
fullHeight = false
|
||||||
}: { post: App.BlogPost; size: BlogCardSize; fullHeight: boolean } = $props();
|
}: { post: App.BlogPost; size?: BlogCardSize; fullHeight?: boolean } = $props();
|
||||||
|
|
||||||
const type: App.BlogPostType = post.type ?? 'article';
|
const type: App.BlogPostType = post.type ?? 'article';
|
||||||
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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -26,17 +31,21 @@ export function postEventStatus(post: App.BlogPost): EventStatus {
|
||||||
export type PostComparer = (a: App.BlogPost, b: App.BlogPost) => number;
|
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 {
|
|
||||||
const dateA = new Date(a).valueOf();
|
|
||||||
if (!b) return dateA;
|
|
||||||
|
|
||||||
const dateB = new Date(b).valueOf();
|
|
||||||
return Math.max(dateA, dateB);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
function laterDate(a: string, ...dates: (string | undefined)[]): number {
|
||||||
|
const dateA = new Date(a).valueOf();
|
||||||
|
if (dates.length <= 0) return dateA;
|
||||||
|
|
||||||
|
let max = dateA;
|
||||||
|
for (const d of dates) {
|
||||||
|
if (!d) continue;
|
||||||
|
const date = new Date(d).valueOf();
|
||||||
|
max = Math.max(max, date);
|
||||||
|
};
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchPostsSorted(postComparer?: PostComparer) {
|
export async function fetchPostsSorted(postComparer?: PostComparer) {
|
||||||
const allPosts = await fetchPosts();
|
const allPosts = await fetchPosts();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,4 +36,7 @@ __Tea Sanctuary__ — это также и сообщество едином
|
||||||
Общие вопросы можно задавать в [сообществе Tea Sanctuary](https://teasanctuary.ru/discord).
|
Общие вопросы можно задавать в [сообществе Tea Sanctuary](https://teasanctuary.ru/discord).
|
||||||
Там же можете написать личное сообщение администраторам.
|
Там же можете написать личное сообщение администраторам.
|
||||||
|
|
||||||
|
Есть и другие способы следить за нашими новостями — например, [канал в Telegram](https://t.me/tea_sanctuary),
|
||||||
|
с комментариями и отдельным чатом.
|
||||||
|
|
||||||
Наши соцсети и почту для более важных обращений можно найти на странице [Контакты](/contact).
|
Наши соцсети и почту для более важных обращений можно найти на странице [Контакты](/contact).
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,24 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SocialButton from '$lib/components/SocialButton.svelte';
|
import SocialButton from '$lib/components/SocialButton.svelte';
|
||||||
import { PUBLIC_TS_DISCORD, PUBLIC_TS_TELEGRAM } from '$env/static/public';
|
import { PUBLIC_TS_DISCORD, PUBLIC_TS_TELEGRAM } from '$env/static/public';
|
||||||
import BlogCard, { BlogCardSize } from '$src/lib/components/BlogCard.svelte';
|
import BlogCard, { BlogCardSize } from '$lib/components/BlogCard.svelte';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { postIsEventOfInterest } from '$lib/util/Blogs';
|
||||||
|
|
||||||
export let data: PageData;
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
|
// postIsEventOfInterest(b) ? b.dateEventTo : undefined
|
||||||
|
let posts: App.BlogPost[] = $state(page.data.posts);
|
||||||
|
onMount(() => {
|
||||||
|
const dateToNum = (d: string) => new Date(d).valueOf();
|
||||||
|
posts = posts.sort(
|
||||||
|
(a, b) =>
|
||||||
|
dateToNum(postIsEventOfInterest(b) ? b.dateEventTo! : b.date!) -
|
||||||
|
dateToNum(postIsEventOfInterest(a) ? a.dateEventTo! : a.date!)
|
||||||
|
);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -88,9 +101,11 @@
|
||||||
<h1 class="font-disket text-center text-2xl font-bold sm:text-4xl">ПОСЛЕДНИЕ ПОСТЫ</h1>
|
<h1 class="font-disket text-center text-2xl font-bold sm:text-4xl">ПОСЛЕДНИЕ ПОСТЫ</h1>
|
||||||
|
|
||||||
<div class="flex flex-row items-stretch justify-evenly gap-4 overflow-x-auto p-4">
|
<div class="flex flex-row items-stretch justify-evenly gap-4 overflow-x-auto p-4">
|
||||||
{#each page.data.posts as post, i}
|
{#each posts as post, i}
|
||||||
<div class="aspect-3/2 w-80 shrink-0">
|
<div class="aspect-3/2 w-80 shrink-0">
|
||||||
|
{#key post}
|
||||||
<BlogCard {post} size={BlogCardSize.Short} fullHeight />
|
<BlogCard {post} size={BlogCardSize.Short} fullHeight />
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { fetchPostsSorted, sortPostsByPostAndUpdateDate } from "$src/lib/util/Blogs";
|
import { fetchPostsSorted, sortPostsByPostAndUpdateDate } from "$src/lib/util/Blogs";
|
||||||
|
|
||||||
const LATEST_POSTS_COUNT = 3;
|
const LATEST_POSTS_COUNT = 5;
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
let md: any
|
let md: any
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { fetchPostsSorted } from "$src/lib/util/Blogs";
|
import { fetchPostsSorted } from "$lib/util/Blogs";
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
return { title: "Блог", description: "Новости и заметки проектов Tea Sanctuary", posts: await fetchPostsSorted() };
|
return { title: "Блог", description: "Новости и заметки проектов Tea Sanctuary", posts: await fetchPostsSorted() };
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import SocialHyperlink from '$src/lib/components/SocialHyperlink.svelte';
|
import SocialHyperlink from '$lib/components/SocialHyperlink.svelte';
|
||||||
import InfoBlock from '$src/lib/components/InfoBlock.svelte';
|
import InfoBlock from '$lib/components/InfoBlock.svelte';
|
||||||
import BlogCard from '$src/lib/components/BlogCard.svelte';
|
import BlogCard, { BlogCardSize } from '$lib/components/BlogCard.svelte';
|
||||||
|
import { postIsEventOfInterest, type PostComparer } from '$lib/util/Blogs';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
function groupPostsByMonthYear(posts: App.BlogPost[]) {
|
function groupPostsByMonthYear(posts: App.BlogPost[]) {
|
||||||
const groupedPosts = new Map<string, App.BlogPost[]>();
|
const groupedPosts = new Map<string, App.BlogPost[]>();
|
||||||
|
|
@ -19,7 +21,20 @@
|
||||||
return groupedPosts;
|
return groupedPosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEventsOfInterest(posts: App.BlogPost[]) {
|
||||||
|
// Сортировка в обратном порядке - чем ближе к завершению, тем раньше в списке
|
||||||
|
const sortPostsByEvent: PostComparer = (a, b) =>
|
||||||
|
new Date(a.dateEventTo!).valueOf() - new Date(b.dateEventTo!).valueOf();
|
||||||
|
|
||||||
|
return posts.filter((a) => postIsEventOfInterest(a)).sort(sortPostsByEvent);
|
||||||
|
}
|
||||||
|
|
||||||
const groupedPosts = groupPostsByMonthYear(page.data.posts);
|
const groupedPosts = groupPostsByMonthYear(page.data.posts);
|
||||||
|
let eventPosts: App.BlogPost[] = $state([]);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
eventPosts = getEventsOfInterest(page.data.posts);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="hero flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4">
|
<section class="hero flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4">
|
||||||
|
|
@ -39,6 +54,20 @@
|
||||||
пропускать новые посты!
|
пропускать новые посты!
|
||||||
</InfoBlock>
|
</InfoBlock>
|
||||||
|
|
||||||
|
{#if !!eventPosts && eventPosts.length > 0}
|
||||||
|
<section class="flex flex-col items-stretch bg-amber-50 pt-4">
|
||||||
|
<h1 class="font-disket text-center text-2xl font-bold sm:text-4xl">АКТУАЛЬНЫЕ СОБЫТИЯ</h1>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-stretch justify-evenly gap-4 overflow-x-auto p-4">
|
||||||
|
{#each eventPosts as post, i}
|
||||||
|
<div class="aspect-3/2 w-80 shrink-0">
|
||||||
|
<BlogCard {post} size={BlogCardSize.Short} fullHeight />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<section class="flex flex-col items-stretch p-2 pb-8 md:p-4">
|
<section class="flex flex-col items-stretch p-2 pb-8 md:p-4">
|
||||||
{#each groupedPosts.entries() as [monthYear, postsInMonthYear]}
|
{#each groupedPosts.entries() as [monthYear, postsInMonthYear]}
|
||||||
<h1
|
<h1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue