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'; import path from 'path';
export const THUMBNAIL_DEFAULT = "https://teasanctuary.ru/common/background-day.webp"; 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 allPosts = await fetchPosts();
const sortedPosts = allPosts const sortedPosts = allPosts
// Для списка постов оставляем только те, у которых объявлена дата публикации // Для списка постов оставляем только те, у которых объявлена дата публикации
.filter((a) => !!a.date) .filter((a) => !!a.date)
.sort((a, b) => { .sort(postComparer ?? sortPostsByPostDate);
return new Date(b.date!).valueOf() - new Date(a.date!).valueOf();
});
return sortedPosts; 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 SocialButton from '$lib/components/SocialButton.svelte';
import SocialHyperlink from '$lib/components/SocialHyperlink.svelte'; import SocialHyperlink from '$lib/components/SocialHyperlink.svelte';
import { PUBLIC_TS_DISCORD } from '$env/static/public'; import { PUBLIC_TS_DISCORD } from '$env/static/public';
import BlogCard, { BlogCardSize } from '$src/lib/components/BlogCard.svelte';
import { page } from '$app/state';
</script> </script>
<svelte:head> <svelte:head>
<title>Tea Sanctuary</title> <title>Tea Sanctuary</title>
</svelte:head> </svelte:head>
<section <section class="hero flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4">
class="flex shrink-0 flex-col items-center justify-center gap-5 overflow-hidden p-4 hero"
>
<div <div
class="flex flex-col flex-nowrap items-center justify-center gap-3 lg:flex-row lg:flex-nowrap" class="flex flex-col flex-nowrap items-center justify-center gap-3 lg:flex-row lg:flex-nowrap"
> >
@ -57,19 +57,12 @@
> >
</div> </div>
<div <div class="text-center font-bold lg:text-left">
class="text-center font-bold lg:text-left"
>
<h1>TEA</h1> <h1>TEA</h1>
<h1>SANCTUARY</h1> <h1>SANCTUARY</h1>
</div> </div>
</div> </div>
<div class="flex flex-col items-center justify-start gap-4"> <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"> <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={PUBLIC_TS_DISCORD}>Сообщество</SocialButton>
<SocialButton class="w-60 shrink-0" href="https://github.com/TeaSanctuary/"> <SocialButton class="w-60 shrink-0" href="https://github.com/TeaSanctuary/">
@ -87,6 +80,19 @@
</SocialButton> </SocialButton>
</div> </div>
</section> </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"> <section class="flex justify-center bg-slate-50 text-slate-950">
<div <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" 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"> <section id="who-are-we">
<h1>Кто мы?</h1> <h1>Кто мы?</h1>
<div class="text-justify"> <div class="text-justify">
<b>Tea Sanctuary</b> &mdash; это в первую очередь коллектив друзей, разрабатывающих проекты <b>Tea Sanctuary</b> &mdash; это в первую очередь коллектив друзей, разрабатывающих
для души, для всеобщего пользования и даже на заказ. С <b>8 июля 2017 года</b> мы ведём публичную проекты для души, для всеобщего пользования и даже на заказ. С
деятельность в сфере разработки ПО и развлечений. <b>8 июля 2017 года</b> мы ведём публичную деятельность в сфере разработки ПО и развлечений.
</div> </div>
<br /> <br />
<div class="text-justify"> <div class="text-justify">
<b>Tea Sanctuary</b> &mdash; это также и сообщество единомышленников. Любовь к добротным видеоиграм <b>Tea Sanctuary</b> &mdash; это также и сообщество единомышленников. Любовь к добротным
и пассивная агрессия к вычислительной технике у нас в крови. Когда-то сообщество было закрытым видеоиграм и пассивная агрессия к вычислительной технике у нас в крови. Когда-то сообщество
и насчитывало около 50 участников, но впоследствии мы решили его расширить. Станьте частью коллектива! было закрытым и насчитывало около 50 участников, но впоследствии мы решили его расширить.
Станьте частью коллектива!
</div> </div>
</section> </section>
<section id="what-are-we-doing"> <section id="what-are-we-doing">
<h1>Что делаем?</h1> <h1>Что делаем?</h1>
<div class="text-justify"> <div class="text-justify">
Наша главная страсть &mdash; это, конечно, видеоигры. Мы часто участвуем в так называемых Наша главная страсть &mdash; это, конечно, видеоигры. Мы часто участвуем в так
"гейм джемах" &mdash; конкурсах на разработку игр. Наши игры вы можете оценить здесь: называемых "гейм джемах" &mdash; конкурсах на разработку игр. Наши игры вы можете
оценить здесь:
<SocialHyperlink href="https://randomtrash.itch.io">RandomTrash</SocialHyperlink> <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>! <SocialHyperlink href={PUBLIC_TS_DISCORD}>сообществе</SocialHyperlink>!
</div> </div>
<br /> <br />
<div class="text-justify"> <div class="text-justify">
Отдельные участники нашего коллектива занимаются модификацией существующих игр, добавляя в Отдельные участники нашего коллектива занимаются модификацией существующих игр,
них новый контент. Например, <b>MegaZerg</b> создаёт оригинальные карты для такой добавляя в них новый контент. Например, <b>MegaZerg</b> создаёт оригинальные карты
бессмертной классики, как Counter-Strike 1.6, и выкладывает их на ресурс GameBanana: для такой бессмертной классики, как Counter-Strike 1.6, и выкладывает их на ресурс
<SocialHyperlink href="https://gamebanana.com/members/2971042">kemist</SocialHyperlink> GameBanana:
<SocialHyperlink href="https://gamebanana.com/members/2971042">
kemist
</SocialHyperlink>
</div> </div>
<br /> <br />
<div class="text-justify"> <div class="text-justify">
Мы размещаем игровые сервера, как постоянные, так и временные для различных событий. Мы размещаем игровые сервера, как постоянные, так и временные для различных событий.
Например, у нас есть сервер Например, у нас есть сервер
<SocialHyperlink href="https://hl.teasanctuary.ru">Tea Sanctuary HLDM</SocialHyperlink>, где <SocialHyperlink href="https://hl.teasanctuary.ru">
вы можете ознакомиться с новыми картами от всего сообщества Half-Life. Tea Sanctuary HLDM
</SocialHyperlink>, где вы можете ознакомиться с новыми картами от всего сообщества
Half-Life.
</div> </div>
<br /> <br />
<div class="text-justify"> <div class="text-justify">
Не одними играми едины, за нашими плечами есть несколько прикладных программ, созданных под Не одними играми едины, за нашими плечами есть несколько прикладных программ,
заказ. Про них ничего особо рассказать не можем, но если вам надо что-нибудь сделать &mdash; созданных под заказ. Про них ничего особо рассказать не можем, но если вам надо
пишите нам! что-нибудь сделать &mdash; пишите нам!
</div> </div>
</section> </section>
<section id="how-can-you-contact-us"> <section id="how-can-you-contact-us">
<h1>Как с вами связаться?</h1> <h1>Как с вами связаться?</h1>
<div class="text-justify"> <div class="text-justify">
Общие вопросы можно задавать в <SocialHyperlink href={PUBLIC_TS_DISCORD}>сообществе Tea Sanctuary</SocialHyperlink>. Общие вопросы можно задавать в
Там же можете написать личное сообщение администраторам. <SocialHyperlink href={PUBLIC_TS_DISCORD}>
сообществе Tea Sanctuary
</SocialHyperlink>. Там же можете написать личное сообщение администраторам.
</div> </div>
<br /> <br />
<div class="text-justify"> <div class="text-justify">
Наши соцсети и почту для более важных обращений можно найти на странице <SocialHyperlink href="/contact">Контакты</SocialHyperlink>. Наши соцсети и почту для более важных обращений можно найти на странице
<SocialHyperlink href="/contact">Контакты</SocialHyperlink>.
</div> </div>
</section> </section>
</div> </div>
</section> </section>
<style> <style>
@import "$src/app.css"; @import '$src/app.css';
section > h1,
section > h2 {
@apply font-disket mb-4 font-bold;
}
section > h1 { section > h1 {
@apply font-disket mb-4 text-2xl font-bold sm:text-4xl; @apply text-2xl sm:text-4xl;
}
section > h2 {
@apply text-xl sm:text-3xl;
} }
</style> </style>

View file

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

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import Icon from '@iconify/svelte';
import SocialHyperlink from '$src/lib/components/SocialHyperlink.svelte'; import SocialHyperlink from '$src/lib/components/SocialHyperlink.svelte';
import InfoBlock from '$src/lib/components/InfoBlock.svelte'; import InfoBlock from '$src/lib/components/InfoBlock.svelte';
import BlogCard from '$src/lib/components/BlogCard.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[]>();
@ -48,44 +48,7 @@
</h1> </h1>
<div class="flex flex-col flex-wrap items-center gap-4"> <div class="flex flex-col flex-wrap items-center gap-4">
{#each postsInMonthYear as post, i} {#each postsInMonthYear as post, i}
<a <BlogCard {post} />
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>
{/each} {/each}
</div> </div>
{/each} {/each}

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import DateWidget from '$src/lib/components/DateWidget.svelte';
import InfoBlock from '$src/lib/components/InfoBlock.svelte'; import InfoBlock from '$src/lib/components/InfoBlock.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
@ -33,29 +34,9 @@
? 'bg-amber-50 text-slate-950' ? 'bg-amber-50 text-slate-950'
: 'bg-red-500 text-slate-50'} sm:flex-row sm:flex-nowrap sm:gap-5" : 'bg-red-500 text-slate-50'} sm:flex-row sm:flex-nowrap sm:gap-5"
> >
<div class="flex items-center"> <DateWidget dateString={data.blogPost.date} type="published" />
<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>
{#if data.blogPost.dateChanged} {#if data.blogPost.dateChanged}
<div class="flex items-center font-bold"> <DateWidget dateString={data.blogPost.dateChanged} type="updated" />
<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>
{/if} {/if}
</section> </section>
@ -80,8 +61,8 @@
prose-headings:mb-4 prose-headings:mb-4
prose-headings:font-bold prose-headings:text-slate-950 prose-h1:text-2xl prose-headings:font-bold prose-headings:text-slate-950 prose-h1:text-2xl
prose-h1:sm:text-4xl prose-h2:text-xl prose-h1:sm:text-4xl prose-h2:text-xl
prose-h2:sm:text-3xl mx-auto prose-h2:sm:text-3xl prose-p:text-justify
prose-p:text-justify mx-auto
w-5xl w-5xl
p-4 p-4
text-base text-base

View file

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