167 lines
4.7 KiB
Svelte
167 lines
4.7 KiB
Svelte
<script lang="ts" module>
|
||
export enum BlogCardSize {
|
||
Short,
|
||
Long,
|
||
Both
|
||
}
|
||
</script>
|
||
|
||
<script lang="ts">
|
||
import {
|
||
BLOG_POST_FRESHNESS_MILLIS,
|
||
blogPostTypeToIcon,
|
||
blogPostTypeToString
|
||
} from '$lib/util/Blogs';
|
||
import DateWidget from '$lib/components/DateWidget.svelte';
|
||
import Icon from '@iconify/svelte';
|
||
import { onMount } from 'svelte';
|
||
|
||
let {
|
||
post,
|
||
size = BlogCardSize.Both,
|
||
fullHeight = false
|
||
}: { post: App.BlogPost; size: BlogCardSize; fullHeight: boolean } = $props();
|
||
|
||
const type: App.BlogPostType = post.type ?? 'article';
|
||
let isPostNew = $state(false);
|
||
let isPostUpdated = $state(false);
|
||
let isPostFresh = $derived(isPostNew || isPostUpdated);
|
||
// TODO: rndtrash: события и их актуальность
|
||
|
||
onMount(() => {
|
||
// rndtrash: Выполняем проверки на клиенте, чтобы плашка не скомпилировалась и потом не потеряла актуальность
|
||
const dateNow = new Date().valueOf();
|
||
isPostNew = dateNow - new Date(post.date!).valueOf() <= BLOG_POST_FRESHNESS_MILLIS;
|
||
isPostUpdated =
|
||
post.dateChanged != null &&
|
||
dateNow - new Date(post.dateChanged).valueOf() <= BLOG_POST_FRESHNESS_MILLIS;
|
||
});
|
||
|
||
/**
|
||
* rndtrash: пришлось дублировать классы с модификатором и без, потому что Tailwind просто не понимает,
|
||
* когда его классы склеивают из нескольких частей
|
||
*/
|
||
|
||
/**
|
||
* Возвращает список классов для полноразмерной плашки.
|
||
*
|
||
* Возвращает пустую строку, если плашка может быть только короткой
|
||
* @param classes
|
||
* @param modClasses
|
||
*/
|
||
function fullClass(classes: string, modClasses: string): string {
|
||
if (size === BlogCardSize.Short) return '';
|
||
return size === BlogCardSize.Both ? modClasses : classes;
|
||
}
|
||
|
||
/**
|
||
* Возвращает список классов для короткой карточки.
|
||
*
|
||
* Возвращает пустую строку, если плашка может быть только полноразмерной
|
||
* @param classes
|
||
* @param modClasses
|
||
*/
|
||
function shortClass(classes: string, modClasses?: string): string {
|
||
if (size === BlogCardSize.Long) return '';
|
||
return size === BlogCardSize.Both && modClasses ? modClasses : classes;
|
||
}
|
||
</script>
|
||
|
||
<a
|
||
href="/blog/{post.slug}"
|
||
class="blog-card
|
||
{fullHeight ? 'min-h-full' : ''}
|
||
{isPostUpdated ? 'updated' : isPostNew ? 'new' : ''}
|
||
{shortClass('flex-col justify-baseline')}
|
||
{fullClass('flex-row justify-stretch', 'sm:flex-row sm:justify-stretch')}"
|
||
>
|
||
<div
|
||
class="relative w-full basis-auto overflow-hidden
|
||
{shortClass('h-32 md:h-48')}
|
||
{fullClass('h-auto basis-1/3', 'sm:h-auto sm:basis-1/3')}"
|
||
>
|
||
{#if post.thumbnail}
|
||
<img
|
||
class="thumbnail"
|
||
src={`/blog/${post.slug}/${post.thumbnail}`}
|
||
alt={post.thumbnailAlt ?? 'Миниатюра поста'}
|
||
/>
|
||
{/if}
|
||
{#if isPostFresh}
|
||
<div class="toast {fullClass('hidden', 'sm:hidden')}">
|
||
{#if isPostUpdated}
|
||
ОБНОВЛЕНО
|
||
{:else}
|
||
НОВОЕ
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
<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">
|
||
<DateWidget
|
||
class={post.dateChanged ? shortClass('hidden', 'not-sm:hidden') : ''}
|
||
dateString={post.date}
|
||
type="published"
|
||
highlight={isPostNew && !isPostUpdated}
|
||
/>
|
||
{#if post.dateChanged}
|
||
<DateWidget
|
||
dateString={post.dateChanged}
|
||
type="updated"
|
||
highlight={isPostUpdated}
|
||
/>
|
||
{/if}
|
||
<div class="flex items-center gap-2 p-1 text-lg font-bold">
|
||
<Icon icon={blogPostTypeToIcon(type)} width={28} height={28} />
|
||
<span class={shortClass('hidden', 'not-md:hidden')}>
|
||
{blogPostTypeToString(type)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h2 class="text-3xl font-bold">{post.title}</h2>
|
||
|
||
{#if post.description}
|
||
<p>{post.description}</p>
|
||
{/if}
|
||
</div>
|
||
</a>
|
||
|
||
<style>
|
||
@import '$src/app.css';
|
||
|
||
.blog-card {
|
||
@apply flex w-full max-w-5xl overflow-hidden rounded-lg bg-slate-100 text-slate-950 drop-shadow-xl transition-all hover:drop-shadow-2xl;
|
||
|
||
.toast {
|
||
@apply absolute top-0 right-0 rounded-bl-lg p-2 font-bold text-slate-50;
|
||
}
|
||
|
||
&.updated {
|
||
box-shadow: 0 0 0 4px var(--color-purple-600);
|
||
|
||
.toast {
|
||
@apply bg-purple-600;
|
||
}
|
||
}
|
||
|
||
&.new {
|
||
box-shadow: 0 0 0 4px var(--color-amber-600);
|
||
|
||
.toast {
|
||
@apply bg-amber-600;
|
||
}
|
||
}
|
||
|
||
img.thumbnail {
|
||
@apply absolute h-full w-full object-cover transition-transform;
|
||
|
||
transform: scale(1);
|
||
}
|
||
|
||
&:hover img.thumbnail {
|
||
transform: scale(1.1);
|
||
}
|
||
}
|
||
</style>
|