Compare commits

..

7 commits

9 changed files with 270 additions and 108 deletions

View file

@ -0,0 +1,143 @@
<script lang="ts" module>
export enum BlogCardSize {
Short,
Long,
Both
}
</script>
<script lang="ts">
import { BLOG_POST_FRESHNESS_MILLIS } from '$lib/util/Blogs';
import DateWidget from '$lib/components/DateWidget.svelte';
export let post: App.BlogPost;
export let size: BlogCardSize = BlogCardSize.Both;
const dateNow = new Date().valueOf();
const isPostNew = dateNow - new Date(post.date!).valueOf() <= BLOG_POST_FRESHNESS_MILLIS;
const isPostUpdated =
post.dateChanged != null &&
dateNow - new Date(post.dateChanged).valueOf() <= BLOG_POST_FRESHNESS_MILLIS;
const isPostFresh = isPostNew || isPostUpdated;
/**
* 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
{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')}
{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 md:p-8">
<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>
<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>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import Icon from '@iconify/svelte';
let className: string = '';
export { className as class };
export let dateString: string | undefined;
export let type: 'published' | 'updated' = 'published';
export let highlight = false;
const icon =
type == 'published' ? 'material-symbols:calendar-today' : 'material-symbols:update';
const highlightClasses = (classes: string) => (highlight ? classes : '');
</script>
<div
class="flex items-center gap-2 p-1 text-lg font-bold {className} rounded-lg
{highlightClasses(type == 'published' ? 'bg-amber-600' : 'bg-purple-600')}
{highlightClasses('text-slate-50')}"
>
<Icon {icon} width={28} height={28} />
<span>
{dateString
? new Date(dateString).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric'
})
: 'Не опубликован!'}
</span>
</div>

View file

@ -1,16 +1,29 @@
import path from 'path';
export const THUMBNAIL_DEFAULT = "https://teasanctuary.ru/common/background-day.webp";
export const BLOG_POST_FRESHNESS_MILLIS = 3 * 24 * 60 * 60 * 1000; // 3 дня
export async function fetchPostsSorted() {
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();
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 async function fetchPostsSorted(postComparer?: PostComparer) {
const allPosts = await fetchPosts();
const sortedPosts = allPosts
// Для списка постов оставляем только те, у которых объявлена дата публикации
.filter((a) => !!a.date)
.sort((a, b) => {
return new Date(b.date!).valueOf() - new Date(a.date!).valueOf();
});
.sort(postComparer ?? sortPostsByPostDate);
return sortedPosts;
};

View file

@ -0,0 +1,7 @@
import { fetchPostsSorted, sortPostsByPostAndUpdateDate } from "$src/lib/util/Blogs";
const LATEST_POSTS_COUNT = 3;
export async function load() {
return { posts: (await fetchPostsSorted(sortPostsByPostAndUpdateDate)).slice(0, LATEST_POSTS_COUNT) };
}

View file

@ -2,15 +2,15 @@
import SocialButton from '$lib/components/SocialButton.svelte';
import SocialHyperlink from '$lib/components/SocialHyperlink.svelte';
import { PUBLIC_TS_DISCORD } from '$env/static/public';
import BlogCard, { BlogCardSize } from '$src/lib/components/BlogCard.svelte';
import { page } from '$app/state';
</script>
<svelte:head>
<title>Tea Sanctuary</title>
</svelte:head>
<section
class="flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4 hero"
>
<section class="hero flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4">
<div
class="flex flex-col flex-nowrap items-center justify-center gap-3 lg:flex-row lg:flex-nowrap"
>
@ -57,19 +57,12 @@
>
</div>
<div
class="text-center font-bold lg:text-left"
>
<div class="text-center font-bold lg:text-left">
<h1>TEA</h1>
<h1>SANCTUARY</h1>
</div>
</div>
<div class="flex flex-col items-center justify-start gap-4">
<!--
<div class="flex flex-row gap-2 rounded-2xl bg-amber-950 p-2 text-center text-amber-50 ring-2 shadow">
Сайт находится в разработке. Если эта плашка висит после 1.06.2025 - пинайте Ивана!
</div>
-->
<div class="flex flex-row flex-wrap items-start justify-center gap-4">
<SocialButton class="w-60 shrink-0" href={PUBLIC_TS_DISCORD}>Сообщество</SocialButton>
<SocialButton class="w-60 shrink-0" href="https://github.com/TeaSanctuary/">
@ -87,6 +80,19 @@
</SocialButton>
</div>
</section>
<section class="flex flex-col items-stretch bg-blue-900 pt-4 text-slate-50">
<h1 class="text-center">ПОСЛЕДНИЕ ПОСТЫ</h1>
<div class="flex flex-row items-stretch justify-evenly gap-4 overflow-x-auto p-4">
{#each page.data.posts as post, i}
<div class="aspect-3/2 w-80 shrink-0">
<BlogCard {post} size={BlogCardSize.Short} />
</div>
{/each}
</div>
</section>
<section class="flex justify-center bg-slate-50 text-slate-950">
<div
class="flex w-5xl max-w-screen flex-col flex-nowrap gap-12 p-2 px-2 pt-12 pb-12 text-base sm:text-xl"
@ -94,67 +100,87 @@
<section id="who-are-we">
<h1>Кто мы?</h1>
<div class="text-justify">
<b>Tea Sanctuary</b> &mdash; это в первую очередь коллектив друзей, разрабатывающих проекты
для души, для всеобщего пользования и даже на заказ. С <b>8 июля 2017 года</b> мы ведём публичную
деятельность в сфере разработки ПО и развлечений.
<b>Tea Sanctuary</b> &mdash; это в первую очередь коллектив друзей, разрабатывающих
проекты для души, для всеобщего пользования и даже на заказ. С
<b>8 июля 2017 года</b> мы ведём публичную деятельность в сфере разработки ПО и развлечений.
</div>
<br />
<div class="text-justify">
<b>Tea Sanctuary</b> &mdash; это также и сообщество единомышленников. Любовь к добротным видеоиграм
и пассивная агрессия к вычислительной технике у нас в крови. Когда-то сообщество было закрытым
и насчитывало около 50 участников, но впоследствии мы решили его расширить. Станьте частью коллектива!
<b>Tea Sanctuary</b> &mdash; это также и сообщество единомышленников. Любовь к добротным
видеоиграм и пассивная агрессия к вычислительной технике у нас в крови. Когда-то сообщество
было закрытым и насчитывало около 50 участников, но впоследствии мы решили его расширить.
Станьте частью коллектива!
</div>
</section>
<section id="what-are-we-doing">
<h1>Что делаем?</h1>
<div class="text-justify">
Наша главная страсть &mdash; это, конечно, видеоигры. Мы часто участвуем в так называемых
"гейм джемах" &mdash; конкурсах на разработку игр. Наши игры вы можете оценить здесь:
Наша главная страсть &mdash; это, конечно, видеоигры. Мы часто участвуем в так
называемых "гейм джемах" &mdash; конкурсах на разработку игр. Наши игры вы можете
оценить здесь:
<SocialHyperlink href="https://randomtrash.itch.io">RandomTrash</SocialHyperlink>
<SocialHyperlink href="https://friendlywithmeat.itch.io/">FriendlyWithMeat</SocialHyperlink>.
Также мы ведём работу над нашим первым полноценным игровым проектом.
Следите за новостями в нашем
<SocialHyperlink href="https://friendlywithmeat.itch.io/">
FriendlyWithMeat
</SocialHyperlink>. Также мы ведём работу над нашим первым полноценным игровым
проектом. Следите за новостями в нашем
<SocialHyperlink href={PUBLIC_TS_DISCORD}>сообществе</SocialHyperlink>!
</div>
<br />
<div class="text-justify">
Отдельные участники нашего коллектива занимаются модификацией существующих игр, добавляя в
них новый контент. Например, <b>MegaZerg</b> создаёт оригинальные карты для такой
бессмертной классики, как Counter-Strike 1.6, и выкладывает их на ресурс GameBanana:
<SocialHyperlink href="https://gamebanana.com/members/2971042">kemist</SocialHyperlink>
Отдельные участники нашего коллектива занимаются модификацией существующих игр,
добавляя в них новый контент. Например, <b>MegaZerg</b> создаёт оригинальные карты
для такой бессмертной классики, как Counter-Strike 1.6, и выкладывает их на ресурс
GameBanana:
<SocialHyperlink href="https://gamebanana.com/members/2971042">
kemist
</SocialHyperlink>
</div>
<br />
<div class="text-justify">
Мы размещаем игровые сервера, как постоянные, так и временные для различных событий.
Например, у нас есть сервер
<SocialHyperlink href="https://hl.teasanctuary.ru">Tea Sanctuary HLDM</SocialHyperlink>, где
вы можете ознакомиться с новыми картами от всего сообщества Half-Life.
<SocialHyperlink href="https://hl.teasanctuary.ru">
Tea Sanctuary HLDM
</SocialHyperlink>, где вы можете ознакомиться с новыми картами от всего сообщества
Half-Life.
</div>
<br />
<div class="text-justify">
Не одними играми едины, за нашими плечами есть несколько прикладных программ, созданных под
заказ. Про них ничего особо рассказать не можем, но если вам надо что-нибудь сделать &mdash;
пишите нам!
Не одними играми едины, за нашими плечами есть несколько прикладных программ,
созданных под заказ. Про них ничего особо рассказать не можем, но если вам надо
что-нибудь сделать &mdash; пишите нам!
</div>
</section>
<section id="how-can-you-contact-us">
<h1>Как с вами связаться?</h1>
<div class="text-justify">
Общие вопросы можно задавать в <SocialHyperlink href={PUBLIC_TS_DISCORD}>сообществе Tea Sanctuary</SocialHyperlink>.
Там же можете написать личное сообщение администраторам.
Общие вопросы можно задавать в
<SocialHyperlink href={PUBLIC_TS_DISCORD}>
сообществе Tea Sanctuary
</SocialHyperlink>. Там же можете написать личное сообщение администраторам.
</div>
<br />
<div class="text-justify">
Наши соцсети и почту для более важных обращений можно найти на странице <SocialHyperlink href="/contact">Контакты</SocialHyperlink>.
Наши соцсети и почту для более важных обращений можно найти на странице
<SocialHyperlink href="/contact">Контакты</SocialHyperlink>.
</div>
</section>
</div>
</section>
<style>
@import "$src/app.css";
@import '$src/app.css';
section > h1,
section > h2 {
@apply font-disket mb-4 font-bold;
}
section > h1 {
@apply font-disket mb-4 text-2xl font-bold sm:text-4xl;
@apply text-2xl sm:text-4xl;
}
</style>
section > h2 {
@apply text-xl sm:text-3xl;
}
</style>

View file

@ -1,3 +0,0 @@
export async function load() {
return { title: undefined };
}

View file

@ -1,8 +1,8 @@
<script lang="ts">
import { page } from '$app/state';
import Icon from '@iconify/svelte';
import SocialHyperlink from '$src/lib/components/SocialHyperlink.svelte';
import InfoBlock from '$src/lib/components/InfoBlock.svelte';
import BlogCard from '$src/lib/components/BlogCard.svelte';
function groupPostsByMonthYear(posts: App.BlogPost[]) {
const groupedPosts = new Map<string, App.BlogPost[]>();
@ -48,44 +48,7 @@
</h1>
<div class="flex flex-col flex-wrap items-center gap-4">
{#each postsInMonthYear as post, i}
<a
href="/blog/{post.slug}"
class="flex w-full max-w-5xl flex-col justify-baseline overflow-hidden rounded-lg bg-slate-100 text-slate-950 drop-shadow-xl transition-all hover:drop-shadow-2xl sm:flex-row sm:justify-stretch"
>
<div class="relative h-32 w-full basis-auto sm:h-auto sm:basis-1/3">
{#if post.thumbnail}
<img
class="absolute h-full w-full object-cover"
src={`/blog/${post.slug}/${post.thumbnail}`}
alt={post.thumbnailAlt ?? 'Миниатюра поста'}
/>
{/if}
</div>
<div class="flex w-full flex-col justify-center p-4 break-words md:p-8">
<div class="flex flex-row flex-wrap justify-between gap-4 pb-4">
<div class="flex items-center text-lg font-bold">
<Icon
icon="material-symbols:calendar-today"
class="mr-3"
style="transform: scale( 1.3 )"
/>
<p>
{new Date(post.date!).toLocaleString('default', {
month: 'short',
day: 'numeric',
year: 'numeric'
})}
</p>
</div>
</div>
<h2 class="text-3xl font-bold">{post.title}</h2>
{#if post.description}
<p>{post.description}</p>
{/if}
</div>
</a>
<BlogCard {post} />
{/each}
</div>
{/each}

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import DateWidget from '$src/lib/components/DateWidget.svelte';
import InfoBlock from '$src/lib/components/InfoBlock.svelte';
import type { PageData } from './$types';
import Icon from '@iconify/svelte';
@ -33,29 +34,9 @@
? 'bg-amber-50 text-slate-950'
: 'bg-red-500 text-slate-50'} sm:flex-row sm:flex-nowrap sm:gap-5"
>
<div class="flex items-center">
<Icon icon="material-symbols:calendar-today" class="mr-3" width={24} height={24} />
<p>
{data.blogPost.date
? new Date(data.blogPost.date).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric'
})
: 'Не опубликован!'}
</p>
</div>
<DateWidget dateString={data.blogPost.date} type="published" />
{#if data.blogPost.dateChanged}
<div class="flex items-center font-bold">
<Icon icon="material-symbols:update" class="mr-3" width={24} height={24} />
<p>
{new Date(data.blogPost.dateChanged).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric'
})}
</p>
</div>
<DateWidget dateString={data.blogPost.dateChanged} type="updated" />
{/if}
</section>
@ -80,8 +61,8 @@
prose-headings:mb-4
prose-headings:font-bold prose-headings:text-slate-950 prose-h1:text-2xl
prose-h1:sm:text-4xl prose-h2:text-xl
prose-h2:sm:text-3xl mx-auto
prose-p:text-justify
prose-h2:sm:text-3xl prose-p:text-justify
mx-auto
w-5xl
p-4
text-base

View file

@ -38,7 +38,7 @@ const config = {
},
extensions: ['.svelte', '.md'],
preprocess: [
vitePreprocess(),
vitePreprocess({ script: true }),
mdsvex({
extensions: ['.md'],
layout: join(__dirname, "./src/lib/components/MdsvexLayout.svelte")