Compare commits
No commits in common. "master" and "feature/events" have entirely different histories.
master
...
feature/ev
23 changed files with 951 additions and 3426 deletions
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
key: ${{ steps.tsru-npm.outputs.cache-primary-key }}
|
||||
|
||||
- name: Populate the .env file
|
||||
run: printf "PUBLIC_TS_DISCORD=%s\nPUBLIC_TS_TELEGRAM=%s\n" "${{ vars.PUBLIC_TS_DISCORD }}" "${{ vars.PUBLIC_TS_TELEGRAM }}" >> .env
|
||||
run: echo "PUBLIC_TS_DISCORD=${{ vars.PUBLIC_TS_DISCORD }}" >> .env
|
||||
|
||||
- name: Build frontend
|
||||
run: npm run build
|
||||
|
|
|
|||
3951
package-lock.json
generated
3951
package-lock.json
generated
File diff suppressed because it is too large
Load diff
50
package.json
50
package.json
|
|
@ -11,42 +11,38 @@
|
|||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/css-svelte": "^1.0.0",
|
||||
"@iconify/json": "^2.2.439",
|
||||
"@iconify/svelte": "^4.2.0",
|
||||
"@react2svelte/swipeable": "^0.1.4",
|
||||
"@svelte-put/dragscroll": "^4.0.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/node": "^25.2.3",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.15.0",
|
||||
"@sveltejs/adapter-auto": "^4.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.17.3",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.0.9",
|
||||
"@types/node": "^22.13.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^3.0.0",
|
||||
"mdsvex": "^0.12.6",
|
||||
"mdsvex-relative-images": "^2.0.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"svelte": "^5.50.2",
|
||||
"svelte-check": "^4.3.6",
|
||||
"mdsvex-relative-images": "^1.0.3",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.20.4",
|
||||
"svelte-check": "^4.1.4",
|
||||
"svelte-disable-preload": "^0.0.3",
|
||||
"svelte-resize-observer-action": "^0.0.4",
|
||||
"svelte-sitemap": "^2.7.1",
|
||||
"sveltekit-autoimport": "^1.8.2",
|
||||
"svelte-sitemap": "^2.7.0",
|
||||
"sveltekit-autoimport": "^1.8.1",
|
||||
"tailwindcss": "^4.0.9",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-icons": "^23.0.1",
|
||||
"vite": "^7.3.1"
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-durationformat": "^0.10.1"
|
||||
"@formatjs/intl-durationformat": "^0.7.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
src/app.d.ts
vendored
8
src/app.d.ts
vendored
|
|
@ -1,14 +1,8 @@
|
|||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="unplugin-icons/types/svelte" />
|
||||
|
||||
import type {
|
||||
DurationFormatConstructor,
|
||||
DurationFormatOptions as _DurationFormatOptions,
|
||||
DurationInput as _DurationInput,
|
||||
} from '@formatjs/intl-durationformat/src/types';
|
||||
import type { Component } from 'svelte';
|
||||
import 'unplugin-icons/types/svelte';
|
||||
|
||||
declare global {
|
||||
// rndtrash: Терпим. https://github.com/microsoft/TypeScript/issues/60608
|
||||
|
|
@ -21,7 +15,7 @@ declare global {
|
|||
namespace App {
|
||||
interface Route {
|
||||
label: string;
|
||||
icon?: Component | string;
|
||||
icon: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
import "@formatjs/intl-durationformat/polyfill.js";
|
||||
|
|
@ -9,12 +9,13 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
BLOG_POST_FRESHNESS_MILLIS,
|
||||
blogPostTypeToIconComponent,
|
||||
blogPostTypeToIcon,
|
||||
blogPostTypeToString,
|
||||
EventStatus,
|
||||
postEventStatus
|
||||
} from '$lib/util/Blogs';
|
||||
import DateWidget from '$lib/components/DateWidget.svelte';
|
||||
import Icon from '@iconify/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
fullHeight = false
|
||||
}: { post: App.BlogPost; size?: BlogCardSize; fullHeight?: boolean } = $props();
|
||||
|
||||
const type: App.BlogPostType = $derived(post.type ?? 'article');
|
||||
const type: App.BlogPostType = post.type ?? 'article';
|
||||
let isPostNew = $state(false);
|
||||
let isPostUpdated = $state(false);
|
||||
let isPostFresh = $derived(isPostNew || isPostUpdated);
|
||||
|
|
@ -34,8 +35,6 @@
|
|||
);
|
||||
let eventHasStarted = $derived(eventStatus === EventStatus.InProgress);
|
||||
|
||||
const PostTypeIcon = $derived(blogPostTypeToIconComponent(type));
|
||||
|
||||
onMount(() => {
|
||||
const dateNow = new Date().valueOf();
|
||||
isPostNew = dateNow - new Date(post.date!).valueOf() <= BLOG_POST_FRESHNESS_MILLIS;
|
||||
|
|
@ -151,7 +150,7 @@
|
|||
/>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2 p-1 text-lg font-bold">
|
||||
<PostTypeIcon width={28} height={28} />
|
||||
<Icon icon={blogPostTypeToIcon(type)} width={28} height={28} />
|
||||
<span class={shortClass('hidden', 'not-md:hidden')}>
|
||||
{blogPostTypeToString(type)}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import { dateFormatLong, dateFormatShort } from '$lib/util/Dates';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
import PublishedIcon from '~icons/material-symbols/calendar-today';
|
||||
import UpdatedIcon from '~icons/material-symbols/update';
|
||||
import EventStartIcon from '~icons/material-symbols/rocket-launch';
|
||||
import EventFinishIcon from '~icons/material-symbols/sports-score';
|
||||
|
||||
let {
|
||||
dateString,
|
||||
|
|
@ -22,13 +17,13 @@
|
|||
} = $props();
|
||||
|
||||
const typeToIcon = {
|
||||
published: PublishedIcon,
|
||||
updated: UpdatedIcon,
|
||||
eventStart: EventStartIcon,
|
||||
eventFinish: EventFinishIcon
|
||||
published: 'material-symbols:calendar-today',
|
||||
updated: 'material-symbols:update',
|
||||
eventStart: 'material-symbols:rocket-launch',
|
||||
eventFinish: 'material-symbols:sports-score'
|
||||
};
|
||||
|
||||
const IconComponent: Component | undefined = $derived(type ? typeToIcon[type] : undefined);
|
||||
const icon = type ? typeToIcon[type] : undefined;
|
||||
|
||||
const highlightClasses = (classes: string) => (highlight ? classes : '');
|
||||
</script>
|
||||
|
|
@ -43,8 +38,8 @@
|
|||
)}
|
||||
{highlightClasses('text-slate-50')}"
|
||||
>
|
||||
{#if IconComponent}
|
||||
<IconComponent width={28} height={28} />
|
||||
{#if icon}
|
||||
<Icon {icon} width={28} height={28} />
|
||||
{/if}
|
||||
<span class="text-nowrap">
|
||||
{dateString
|
||||
|
|
|
|||
|
|
@ -1,23 +1,12 @@
|
|||
<script lang="ts">
|
||||
import type { Component } from 'svelte';
|
||||
import Icon from '@iconify/svelte';
|
||||
|
||||
let {
|
||||
class: klass = '',
|
||||
src,
|
||||
alt = '',
|
||||
size = 32,
|
||||
black = false
|
||||
}: {
|
||||
class?: string;
|
||||
src?: Component | string;
|
||||
alt?: string;
|
||||
size?: number;
|
||||
black?: boolean;
|
||||
} = $props();
|
||||
|
||||
const IconComponent: Component | undefined = $derived(
|
||||
typeof src === 'function' ? src : undefined
|
||||
);
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
export let src: string | null = null;
|
||||
export let alt: string = "";
|
||||
export let size: number = 32;
|
||||
export let black: boolean = false;
|
||||
|
||||
function isUrl(src?: string) {
|
||||
return src?.startsWith('/') || src?.startsWith('http');
|
||||
|
|
@ -25,14 +14,16 @@
|
|||
</script>
|
||||
|
||||
<span
|
||||
class="{klass} {black ? 'fill-slate-950' : 'fill-slate-50'} hover-icon"
|
||||
class="{className} {black ? 'fill-slate-950' : 'fill-slate-50'} hover-icon"
|
||||
style:width="{size}px"
|
||||
style:height="{size}px"
|
||||
>
|
||||
{#if IconComponent}
|
||||
<IconComponent width={size} height={size} color={black ? '#020618' : '#f8fafc'} />
|
||||
{:else if typeof src === 'string' && isUrl(src)}
|
||||
<img {src} {alt} width={size} height={size} />
|
||||
{#if src}
|
||||
{#if isUrl(src)}
|
||||
<img {src} {alt} width={size} height={size} />
|
||||
{:else}
|
||||
<Icon width={size} height={size} icon={src} color={black ? '#020618' : '#f8fafc'} />
|
||||
{/if}
|
||||
{:else}
|
||||
{alt ?? 'Без иконки'}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,18 @@
|
|||
<script lang="ts">
|
||||
import type { Component, Snippet } from 'svelte';
|
||||
import Icon from '@iconify/svelte';
|
||||
|
||||
let {
|
||||
icon: IconComponent,
|
||||
bgStrong,
|
||||
bgBleak,
|
||||
caption,
|
||||
children
|
||||
}: {
|
||||
icon: Component;
|
||||
bgStrong: string;
|
||||
bgBleak: string;
|
||||
caption: string;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
export let bgStrong: string;
|
||||
export let bgBleak: string;
|
||||
export let icon: string;
|
||||
export let caption: string;
|
||||
</script>
|
||||
|
||||
<section class="flex flex-col sm:flex-row">
|
||||
<div class="flex flex-row items-center gap-2 {bgStrong} p-2 text-slate-50">
|
||||
<IconComponent width={32} height={32} color={'#f8fafc'} />
|
||||
<Icon width={32} height={32} {icon} color={'#f8fafc'} />
|
||||
<span class="sm:hidden">{caption}</span>
|
||||
</div>
|
||||
<div class="{bgBleak} p-4 sm:grow">
|
||||
{@render children()}
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import IconBlock from './IconBlock.svelte';
|
||||
import InfoIcon from '~icons/material-symbols/info';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<IconBlock bgStrong="bg-blue-500" bgBleak="bg-blue-50" icon={InfoIcon} caption="Примечание">
|
||||
{@render children()}
|
||||
<IconBlock
|
||||
bgStrong="bg-blue-500"
|
||||
bgBleak="bg-blue-50"
|
||||
icon="material-symbols:info"
|
||||
caption="Обратите внимание"
|
||||
>
|
||||
<slot />
|
||||
</IconBlock>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import HoverIcon from '$lib/components/HoverIcon.svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let { routes }: { routes: App.Route[] } = $props();
|
||||
export let routes: App.Route[];
|
||||
|
||||
function isActive(route: string): boolean {
|
||||
if (route === '/') return page.url.pathname === route;
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
</script>
|
||||
|
||||
<nav
|
||||
class="flex shrink-0 flex-row gap-2 bg-slate-100 not-landscape:justify-around not-landscape:overflow-x-auto not-landscape:sm:px-2 landscape:flex-col landscape:overflow-y-auto landscape:sm:py-2"
|
||||
class="flex shrink-0 flex-row gap-2 bg-slate-100 not-landscape:justify-around not-landscape:overflow-x-auto not-landscape:px-2 landscape:flex-col landscape:overflow-y-auto landscape:py-2"
|
||||
>
|
||||
{#each routes as route (route.href)}
|
||||
<a class="nav-button {isActive(route.href) ? 'active' : ''}" href={route.href}>
|
||||
|
|
@ -36,10 +36,10 @@
|
|||
@import '$src/app.css';
|
||||
|
||||
.nav-button {
|
||||
@apply flex aspect-square shrink-0 flex-col items-center justify-center gap-0.5 p-1 text-slate-950 not-landscape:h-20 hover:bg-emerald-400 sm:p-2 not-landscape:sm:h-24 landscape:w-20 landscape:sm:w-24;
|
||||
@apply flex aspect-square shrink-0 flex-col items-center justify-center gap-0.5 p-2 text-slate-950 not-landscape:h-24 hover:bg-emerald-400 landscape:w-24;
|
||||
|
||||
> .contour {
|
||||
@apply rounded-full px-1 py-0.5 text-center sm:px-1.5;
|
||||
@apply rounded-full px-1.5 py-0.5 text-center;
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,24 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { isLinkLocal, tryGetIcon } from '$lib/util/LinkResolver';
|
||||
import HoverIcon from '$lib/components/HoverIcon.svelte';
|
||||
import HoverIcon from './HoverIcon.svelte';
|
||||
|
||||
let {
|
||||
href,
|
||||
class: klass = '',
|
||||
customIcon = null,
|
||||
children
|
||||
}: { href: string; class?: string; customIcon?: string | null; children: Snippet } = $props();
|
||||
|
||||
const iconSrc = $derived(customIcon ?? tryGetIcon(href));
|
||||
export let href: string;
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
export let customIcon: string | null = null;
|
||||
</script>
|
||||
|
||||
<a
|
||||
{href}
|
||||
class="{klass} flex flex-row drop-shadow-2xl transition-all hover:scale-110"
|
||||
class="{className} flex flex-row drop-shadow-2xl transition-all hover:scale-110"
|
||||
target={isLinkLocal(href) ? '_self' : '_blank'}
|
||||
>
|
||||
<div class="shrink-0 rounded-l-xl bg-slate-800 p-2">
|
||||
<HoverIcon src={iconSrc} class="text-sm uppercase" />
|
||||
<HoverIcon src={customIcon ?? tryGetIcon(href)} class="text-sm uppercase" />
|
||||
</div>
|
||||
<div
|
||||
class="flex shrink-0 grow flex-nowrap items-center justify-center rounded-r-xl bg-slate-100 p-2 text-2xl text-nowrap text-slate-950"
|
||||
>
|
||||
{@render children()}
|
||||
<slot />
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
|
|
@ -1,32 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { MediaQuery } from 'svelte/reactivity';
|
||||
import HoverIcon from './HoverIcon.svelte';
|
||||
import { isLinkLocal, tryGetIcon } from '$lib/util/LinkResolver';
|
||||
|
||||
let {
|
||||
href,
|
||||
class: klass = '',
|
||||
customIcon = null,
|
||||
children
|
||||
}: { href: string; class?: string; customIcon?: string | null; children: Snippet } = $props();
|
||||
|
||||
const iconSrc = $derived(customIcon ?? tryGetIcon(href));
|
||||
export let href: string;
|
||||
let className: string = '';
|
||||
export { className as class };
|
||||
export let customIcon: string | null = null;
|
||||
|
||||
const sm = new MediaQuery('width >= 40rem', false);
|
||||
</script>
|
||||
|
||||
<a
|
||||
{href}
|
||||
class="{klass} group inline-block no-underline"
|
||||
target={isLinkLocal(href) ? '_self' : '_blank'}
|
||||
>
|
||||
<a {href} class="{className} group inline-block no-underline" target={isLinkLocal(href) ? '_self' : '_blank'}>
|
||||
<span
|
||||
class="inline-block size-6 rounded-sm bg-emerald-800 p-0.5 align-bottom transition-all group-hover:scale-110 sm:size-8 sm:rounded-xl sm:p-1"
|
||||
>
|
||||
<HoverIcon src={iconSrc} size={sm.current ? 24 : 20} />
|
||||
<HoverIcon src={customIcon ?? tryGetIcon(href)} size={sm.current ? 24 : 20} />
|
||||
</span>
|
||||
<span class="text-emerald-900 underline">
|
||||
{@render children()}
|
||||
<slot />
|
||||
</span>
|
||||
</a>
|
||||
</a>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import IconBlock from './IconBlock.svelte';
|
||||
import WarningIcon from '~icons/material-symbols/warning';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<IconBlock bgStrong="bg-yellow-500" bgBleak="bg-yellow-50" icon={WarningIcon} caption="Внимание">
|
||||
{@render children()}
|
||||
<IconBlock
|
||||
bgStrong="bg-yellow-500"
|
||||
bgBleak="bg-yellow-50"
|
||||
icon="material-symbols:warning"
|
||||
caption="Внимание"
|
||||
>
|
||||
<slot />
|
||||
</IconBlock>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import type { Component } from "svelte";
|
||||
|
||||
export const THUMBNAIL_DEFAULT = "https://teasanctuary.ru/common/background-day.webp";
|
||||
export const BLOG_POST_FRESHNESS_MILLIS = 3 * 24 * 60 * 60 * 1000; // 3 дня
|
||||
|
||||
|
|
@ -99,20 +97,16 @@ const bptToString: Record<App.BlogPostType, string> = {
|
|||
'update': 'Обновление'
|
||||
};
|
||||
|
||||
import ArticleIcon from '~icons/material-symbols/article';
|
||||
import EventIcon from '~icons/material-symbols/event';
|
||||
import UpdateIcon from '~icons/material-symbols/autorenew';
|
||||
|
||||
const bptToIcon: Record<App.BlogPostType, Component> = {
|
||||
'article': ArticleIcon,
|
||||
'event': EventIcon,
|
||||
'update': UpdateIcon
|
||||
const bptToIcon: Record<App.BlogPostType, string> = {
|
||||
'article': 'material-symbols:article',
|
||||
'event': 'material-symbols:event',
|
||||
'update': 'material-symbols:autorenew'
|
||||
};
|
||||
|
||||
export function blogPostTypeToString(type: App.BlogPostType): string {
|
||||
return bptToString[type] ?? bptToString['article'];
|
||||
}
|
||||
|
||||
export function blogPostTypeToIconComponent(type: App.BlogPostType): Component {
|
||||
export function blogPostTypeToIcon(type: App.BlogPostType): string {
|
||||
return bptToIcon[type] ?? bptToIcon['article'];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
import { shouldPolyfill } from '@formatjs/intl-durationformat/should-polyfill'
|
||||
if (shouldPolyfill()) {
|
||||
import('@formatjs/intl-durationformat/polyfill-force');
|
||||
}
|
||||
|
||||
export const dateFormatShort = new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
|
|
|
|||
|
|
@ -1,54 +1,34 @@
|
|||
import type { Component } from "svelte";
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
import DefaultLinkIcon from '~icons/material-symbols/link';
|
||||
import SteamIcon from '~icons/simple-icons/steam';
|
||||
import TelegramIcon from '~icons/simple-icons/telegram';
|
||||
import TwitterIcon from '~icons/simple-icons/x';
|
||||
import GitHubIcon from '~icons/simple-icons/github';
|
||||
import YouTubeIcon from '~icons/simple-icons/youtube';
|
||||
import ItchIoIcon from '~icons/simple-icons/itchdotio';
|
||||
import DiscordIcon from '~icons/simple-icons/discord';
|
||||
import GameBananaIcon from '~icons/simple-icons/gamebanana';
|
||||
import BlueSkyIcon from '~icons/simple-icons/bluesky';
|
||||
import HamsterIcon from '~icons/fluent-emoji-high-contrast/hamster';
|
||||
import GitIcon from '~icons/devicon-plain/git';
|
||||
import EmailIcon from '~icons/material-symbols/alternate-email';
|
||||
import RssIcon from '~icons/material-symbols/rss-feed';
|
||||
|
||||
type LinkIcon = Component | string;
|
||||
|
||||
// TODO: чашки на иконках выглядят страшненько, пока что пусть лучше будет голая ссылка
|
||||
const icons: Record<string, LinkIcon> = {
|
||||
none: DefaultLinkIcon,
|
||||
'steamcommunity.com': SteamIcon,
|
||||
'steampowered.com': SteamIcon,
|
||||
't.me': TelegramIcon,
|
||||
'twitter.com': TwitterIcon,
|
||||
'x.com': TwitterIcon,
|
||||
'github.com': GitHubIcon,
|
||||
'youtube.com': YouTubeIcon,
|
||||
'itch.io': ItchIoIcon,
|
||||
'discord.com': DiscordIcon,
|
||||
'discord.gg': DiscordIcon,
|
||||
'gamebanana.com': GameBananaIcon,
|
||||
'bsky.app': BlueSkyIcon,
|
||||
'bsky.social': BlueSkyIcon,
|
||||
const icons: Record<string, string> = {
|
||||
none: 'material-symbols:link',
|
||||
'steamcommunity.com': 'simple-icons:steam',
|
||||
'steampowered.com': 'simple-icons:steam',
|
||||
't.me': 'simple-icons:telegram',
|
||||
'twitter.com': 'simple-icons:x',
|
||||
'x.com': 'simple-icons:x',
|
||||
'github.com': 'simple-icons:github',
|
||||
'youtube.com': 'simple-icons:youtube',
|
||||
'itch.io': 'simple-icons:itchdotio',
|
||||
'discord.com': 'simple-icons:discord',
|
||||
'discord.gg': 'simple-icons:discord',
|
||||
'gamebanana.com': 'simple-icons:gamebanana',
|
||||
'bsky.app': 'simple-icons:bluesky',
|
||||
'bsky.social': 'simple-icons:bluesky',
|
||||
// https://хамяк.рф
|
||||
'xn--80auf8a2c.xn--p1ai': HamsterIcon,
|
||||
'xn--80auf8a2c.xn--p1ai': 'fluent-emoji-high-contrast:hamster',
|
||||
// 'teasanctuary.ru': '/icons/tea-sanctuary-white.svg',
|
||||
'hl.teasanctuary.ru': '/icons/half-life.svg',
|
||||
'git.teasanctuary.ru': GitIcon,
|
||||
'git.teasanctuary.ru': 'devicon-plain:git',
|
||||
// localhost: '/icons/tea-sanctuary-white.svg',
|
||||
email: EmailIcon,
|
||||
rss: RssIcon
|
||||
email: 'material-symbols:alternate-email',
|
||||
rss: 'material-symbols:rss-feed'
|
||||
};
|
||||
|
||||
// Особые случаи, когда одним доменом второго уровня не ограничишься (например, randomtrash.itch.io)
|
||||
const specialResolvers: Record<string, (url: URL) => string> = {
|
||||
'teasanctuary.ru': (url) => {
|
||||
// Домены третьего уровня и выше
|
||||
const prefix = url.hostname.split('.').reverse();
|
||||
const prefix = url.hostname.split('.').toReversed();
|
||||
prefix.shift();
|
||||
prefix.shift();
|
||||
if (prefix[0] === "hl") {
|
||||
|
|
@ -71,9 +51,7 @@ const specialResolvers: Record<string, (url: URL) => string> = {
|
|||
'steampowered.com': (url) => 'steampowered.com',
|
||||
}
|
||||
|
||||
function getIconNameFromUrl(url?: URL): string | undefined {
|
||||
if (!url) return undefined;
|
||||
|
||||
function getIconFromUrl(url: URL): string | undefined {
|
||||
const href = url.href;
|
||||
if (href.startsWith('mailto:'))
|
||||
return 'email';
|
||||
|
|
@ -88,16 +66,15 @@ function getIconNameFromUrl(url?: URL): string | undefined {
|
|||
return hostname;
|
||||
}
|
||||
|
||||
const baseURI = browser ? document.baseURI : 'https://teasanctuary.ru';
|
||||
export function tryGetIcon(link: string): LinkIcon {
|
||||
export function tryGetIcon(link: string): string {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(link, baseURI);
|
||||
url = new URL(link, document.baseURI);
|
||||
} catch {
|
||||
return icons['none'];
|
||||
}
|
||||
|
||||
return icons[getIconNameFromUrl(url) ?? ''] ?? icons['none'];
|
||||
return icons[getIconFromUrl(url) ?? ''] ?? icons['none'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,42 +3,13 @@
|
|||
import { page } from '$app/state';
|
||||
import '$src/syntax-highlight.css'; // https://github.com/PrismJS/prism-themes
|
||||
import NavBar from '$lib/components/NavBar.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
import TeaIcon from '~icons/material-symbols/emoji-food-beverage';
|
||||
import TeamIcon from '~icons/material-symbols/groups';
|
||||
import BlogIcon from '~icons/material-symbols/newspaper';
|
||||
import ProjectsIcon from '~icons/material-symbols/work';
|
||||
import ContactsIcon from '~icons/material-symbols/chat';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
|
||||
const routes: App.Route[] = [
|
||||
{
|
||||
label: 'главная',
|
||||
icon: TeaIcon,
|
||||
href: '/'
|
||||
},
|
||||
{
|
||||
label: 'команда',
|
||||
icon: TeamIcon,
|
||||
href: '/team'
|
||||
},
|
||||
{
|
||||
label: 'блог',
|
||||
icon: BlogIcon,
|
||||
href: '/blog'
|
||||
},
|
||||
{
|
||||
label: 'проекты',
|
||||
icon: ProjectsIcon,
|
||||
href: '/projects'
|
||||
},
|
||||
{
|
||||
label: 'контакты',
|
||||
icon: ContactsIcon,
|
||||
href: '/contact'
|
||||
}
|
||||
{ label: 'главная', icon: 'material-symbols:emoji-food-beverage', href: '/' },
|
||||
{ label: 'команда', icon: 'material-symbols:groups', href: '/team' },
|
||||
{ label: 'блог', icon: 'material-symbols:newspaper', href: '/blog' },
|
||||
{ label: 'проекты', icon: 'material-symbols:work', href: '/projects' },
|
||||
{ label: 'контакты', icon: 'material-symbols:chat', href: '/contact' }
|
||||
];
|
||||
</script>
|
||||
|
||||
|
|
@ -62,7 +33,7 @@
|
|||
<NavBar {routes} />
|
||||
<div class="flex grow-1 flex-col overflow-auto">
|
||||
<div class="relative grow-1">
|
||||
{@render children()}
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<footer class="bg-emerald-950">
|
||||
|
|
@ -70,7 +41,7 @@
|
|||
class="mx-auto w-full max-w-screen-xl justify-center px-2 py-6 text-center text-emerald-50 md:flex"
|
||||
>
|
||||
<p>
|
||||
<span class="font-bold">© 2026 Tea Sanctuary</span>
|
||||
<span class="font-bold">© 2025 Tea Sanctuary</span>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -136,6 +136,6 @@
|
|||
text-slate-950 sm:px-4 sm:text-xl"
|
||||
>
|
||||
<section class="mx-auto flex max-w-5xl flex-col flex-nowrap">
|
||||
<data.content />
|
||||
<svelte:component this={data.content} />
|
||||
</section>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
import { page } from '$app/state';
|
||||
import DateWidget from '$lib/components/DateWidget.svelte';
|
||||
import InfoBlock from '$lib/components/InfoBlock.svelte';
|
||||
import PersonIcon from '~icons/material-symbols/person';
|
||||
import Icon from '@iconify/svelte';
|
||||
import type { PageData } from './$types';
|
||||
import {
|
||||
blogPostTypeToIconComponent,
|
||||
blogPostTypeToIcon,
|
||||
blogPostTypeToString,
|
||||
EventStatus,
|
||||
postEventStatus
|
||||
|
|
@ -16,19 +16,16 @@
|
|||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const isPublic = $derived(!!data.blogPost.date);
|
||||
const authors = $derived(
|
||||
const isPublic = !!data.blogPost.date;
|
||||
const authors =
|
||||
data.blogPost.authors == null
|
||||
? []
|
||||
: typeof data.blogPost.authors === 'string'
|
||||
? [data.blogPost.authors]
|
||||
: data.blogPost.authors
|
||||
);
|
||||
const type: App.BlogPostType = $derived(data.blogPost.type ?? 'article');
|
||||
: data.blogPost.authors;
|
||||
const type: App.BlogPostType = data.blogPost.type ?? 'article';
|
||||
let eventStatus = $state(EventStatus.NotEvent);
|
||||
|
||||
const PostTypeIcon = $derived(blogPostTypeToIconComponent(type));
|
||||
|
||||
onMount(() => {
|
||||
// HACK: rndtrash: NodeJS использует другой тип данных для таймаутов
|
||||
let eventTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
|
|
@ -75,16 +72,16 @@
|
|||
</section>
|
||||
|
||||
<section
|
||||
class="flex shrink-0 flex-row flex-wrap items-center justify-center p-2 font-bold {isPublic
|
||||
class="flex shrink-0 flex-col flex-wrap items-center justify-center p-2 font-bold {isPublic
|
||||
? 'bg-amber-50 text-slate-950'
|
||||
: 'bg-red-500 text-slate-50'} sm:gap-x-5"
|
||||
: 'bg-red-500 text-slate-50'} sm:flex-row sm:gap-x-5"
|
||||
>
|
||||
<DateWidget dateString={data.blogPost.date} type="published" />
|
||||
{#if data.blogPost.dateChanged}
|
||||
<DateWidget dateString={data.blogPost.dateChanged} type="updated" />
|
||||
{/if}
|
||||
<div class="flex items-center gap-2 p-1 text-lg font-bold">
|
||||
<PostTypeIcon width={28} height={28} />
|
||||
<Icon icon={blogPostTypeToIcon(type)} width={28} height={28} />
|
||||
<span>
|
||||
{blogPostTypeToString(type)}
|
||||
</span>
|
||||
|
|
@ -92,7 +89,7 @@
|
|||
{#each authors as author}
|
||||
<!-- TODO: rndtrash: из-за 404 не даёт собрать сайт. href="/team/{author}" -->
|
||||
<a class="flex items-center gap-2 p-1 text-lg font-bold" href="#">
|
||||
<PersonIcon width={28} height={28} />
|
||||
<Icon icon="material-symbols:person" width={28} height={28} />
|
||||
<span class="underline">
|
||||
{author}
|
||||
</span>
|
||||
|
|
@ -159,7 +156,7 @@
|
|||
class="prose
|
||||
sm:prose-xl
|
||||
prose-slate
|
||||
prose-code:wrap-break-word
|
||||
prose-code:break-words
|
||||
prose-pre:drop-shadow-md
|
||||
prose-headings:font-disket
|
||||
prose-headings:mb-4
|
||||
|
|
@ -173,5 +170,5 @@
|
|||
text-slate-950
|
||||
sm:text-xl lg:p-8"
|
||||
>
|
||||
<data.content />
|
||||
<svelte:component this={data.content} />
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ function makeEventDescription(post: App.BlogPost): string {
|
|||
return '';
|
||||
|
||||
const dateToUtcString = (s: string) => new Date(s).toUTCString();
|
||||
return `<br><br>Событие проводится с ${dateToUtcString(post.dateEventFrom!)} по ${dateToUtcString(post.dateEventTo!)}.`;
|
||||
return `<br><br>Событие проводится с ${dateToUtcString(post.dateEventFrom!)} по ${dateToUtcString(post.dateEventFrom!)}.`;
|
||||
}
|
||||
|
||||
export async function GET({ setHeaders }) {
|
||||
|
|
|
|||
9
vite.config.js
Normal file
9
vite.config.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
tailwindcss()
|
||||
]
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import type { UserConfig } from 'vite';
|
||||
import legacy from '@vitejs/plugin-legacy';
|
||||
import Icons from 'unplugin-icons/vite';
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
Icons({
|
||||
compiler: 'svelte',
|
||||
}),
|
||||
tailwindcss(),
|
||||
legacy({
|
||||
renderLegacyChunks: false
|
||||
})
|
||||
]
|
||||
} satisfies UserConfig;
|
||||
Loading…
Add table
Add a link
Reference in a new issue