chore: prepare production deployment

This commit is contained in:
2026-07-20 05:24:12 +05:00
parent 2e3550a4dc
commit 77ed2d8423
31 changed files with 681 additions and 496 deletions
+4 -4
View File
@@ -39,7 +39,7 @@ export const posts: Post[] = [
{ type: 'heading', text: 'Stop Loss: где ваша позиция неправа' },
{
type: 'paragraph',
text: 'Stop Loss — уровень, при достижении которого позиция закрывается автоматически. Это не признание ошибки, а часть стратегии. На графиках Flamy AI уровень SL рассчитывается моделью на основе исторической волатильности инструмента.'
text: 'Stop Loss — уровень, при достижении которого позиция закрывается автоматически. Это не признание ошибки, а часть стратегии. На графиках Flamy Trade уровень SL рассчитывается моделью на основе исторической волатильности инструмента.'
},
{ type: 'heading', text: 'Take Profit: когда забирать прибыль' },
{
@@ -70,13 +70,13 @@ export const posts: Post[] = [
id: 2,
slug: 'kak-chitat-grafiki-flamy-ai',
date: '2026-05-17',
title: 'Как читать графики Flamy AI',
description: 'Краткое объяснение свечей, прогнозной зоны, TP и SL на графиках Flamy AI.',
title: 'Как читать графики Flamy Trade',
description: 'Краткое объяснение свечей, прогнозной зоны, TP и SL на графиках Flamy Trade.',
tags: ['ML', 'FORECAST', 'CHARTS'],
content: [
{
type: 'paragraph',
text: 'Графики Flamy AI содержат три слоя: исторические свечи, прогнозную зону и уровни торгового плана. Разберём каждый из них.'
text: 'Графики Flamy Trade содержат три слоя: исторические свечи, прогнозную зону и уровни торгового плана. Разберём каждый из них.'
},
{ type: 'heading', text: 'Исторические свечи' },
{
+2 -2
View File
@@ -7,6 +7,7 @@
CrosshairMode,
LineStyle,
type IChartApi,
type IPriceLine,
type ISeriesApi,
type MouseEventParams,
type UTCTimestamp
@@ -25,8 +26,7 @@
let chart: IChartApi | undefined;
let candleSeries: ISeriesApi<'Candlestick'> | undefined;
let predSeries: ISeriesApi<'Line'> | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let priceLines: any[] = [];
let priceLines: IPriceLine[] = [];
type Tooltip = {
visible: boolean;
+40 -4
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import { tick } from 'svelte';
import { onMount } from 'svelte';
import { gsap } from 'gsap';
import { cn } from '$lib/utils';
@@ -13,6 +15,9 @@
let mobileOpen = $state(false);
let header = $state<HTMLElement>(null!);
let menuButton = $state<HTMLButtonElement>(null!);
const mobileMenuId = 'mobile-navigation';
onMount(() => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
@@ -24,8 +29,33 @@
clearProps: 'transform,opacity'
});
});
function isActive(href: string) {
const pathname = page.url.pathname;
return href === '/' ? pathname === href : pathname === href || pathname.startsWith(`${href}/`);
}
async function toggleMobileMenu() {
mobileOpen = !mobileOpen;
if (mobileOpen) {
await tick();
header.querySelector<HTMLAnchorElement>(`#${mobileMenuId} a`)?.focus();
}
}
function closeMobileMenu({ restoreFocus = false } = {}) {
mobileOpen = false;
if (restoreFocus) menuButton?.focus();
}
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key !== 'Escape' || !mobileOpen) return;
closeMobileMenu({ restoreFocus: true });
}
</script>
<svelte:window onkeydown={handleWindowKeydown} />
<header bind:this={header} class="fixed right-0 left-0 z-9999 border-b border-b-bg-h bg-bg-e">
<div class="mx-auto flex max-w-400 items-center justify-between px-5 py-5 sm:px-10 lg:px-5">
<Logo />
@@ -37,6 +67,7 @@
<a
href={link.href}
class={cn('transition-colors duration-300', link.isButton ? 'nav-btn' : 'nav-link')}
aria-current={isActive(link.href) ? 'page' : undefined}
>
{link.text}
</a>
@@ -46,10 +77,13 @@
</nav>
<button
bind:this={menuButton}
type="button"
class="flex flex-col justify-center gap-1.5 p-1 lg:hidden"
onclick={() => (mobileOpen = !mobileOpen)}
onclick={toggleMobileMenu}
aria-label={mobileOpen ? 'Закрыть меню' : 'Открыть меню'}
aria-expanded={mobileOpen}
aria-controls={mobileMenuId}
>
<span
class={cn(
@@ -74,7 +108,7 @@
<!-- Мобильное выпадающее меню -->
{#if mobileOpen}
<nav class="border-t border-white/6 px-5 pb-4 sm:px-10 lg:hidden">
<nav id={mobileMenuId} class="border-t border-white/6 px-5 pb-4 sm:px-10 lg:hidden">
<ul class="flex flex-col">
{#each menuLinks as link (link.id)}
<li>
@@ -82,7 +116,8 @@
<a
href={link.href}
class="nav-btn mt-3 block text-center"
onclick={() => (mobileOpen = false)}
onclick={() => closeMobileMenu()}
aria-current={isActive(link.href) ? 'page' : undefined}
>
{link.text}
</a>
@@ -90,7 +125,8 @@
<a
href={link.href}
class="nav-link block border-b border-white/6 py-3.5 last:border-0"
onclick={() => (mobileOpen = false)}
onclick={() => closeMobileMenu()}
aria-current={isActive(link.href) ? 'page' : undefined}
>
{link.text}
</a>
+34 -18
View File
@@ -5,29 +5,45 @@
type Props = {
href?: string;
target?: string;
type?: 'button' | 'submit' | 'reset';
variant?: 'primary' | 'secondary';
class?: string;
children: Snippet;
};
let { href, target = '_self', variant = 'primary', class: className, children }: Props = $props();
let {
href,
target = '_self',
type = 'button',
variant = 'primary',
class: className,
children
}: Props = $props();
const classes = $derived(
cn(
'flex w-fit items-center justify-center gap-2 rounded-xl px-6 py-3 font-display text-lg font-medium transition-colors duration-300',
variant === 'primary' && 'bg-primary hover:bg-primary-h',
variant === 'secondary' && 'border bg-bg hover:text-desc',
className
)
);
</script>
<svelte:element
this={href ? 'a' : 'button'}
{href}
{target}
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
class={cn(
'flex w-fit items-center justify-center gap-2 rounded-xl px-6 py-3 font-display text-lg font-medium transition-colors duration-300',
variant === 'primary' && 'bg-primary hover:bg-primary-h',
variant === 'secondary' && 'border bg-bg hover:text-desc',
className
)}
>
{@render children()}
{#if href}
<a {href} {target} rel={target === '_blank' ? 'noopener noreferrer' : undefined} class={classes}>
{@render children()}
{#if variant === 'primary'}
<img src="images/icons/arrow-right.svg" alt="Перейти" class="h-5 w-5" />
{/if}
</svelte:element>
{#if variant === 'primary'}
<img src="/images/icons/arrow-right.svg" alt="" aria-hidden="true" class="h-5 w-5" />
{/if}
</a>
{:else}
<button {type} class={classes}>
{@render children()}
{#if variant === 'primary'}
<img src="/images/icons/arrow-right.svg" alt="" aria-hidden="true" class="h-5 w-5" />
{/if}
</button>
{/if}
+1 -1
View File
@@ -2,7 +2,7 @@
</script>
<a href="/" class="flex max-w-fit items-center gap-2.5 no-underline">
<img src="images/icons/logo.svg" alt="Логотип" class="h-8 w-8" />
<img src="/images/icons/logo.svg" alt="" aria-hidden="true" class="h-8 w-8" />
<span class="font-display text-xl font-black tracking-tighter">
FLAMY<span class="text-primary">TRADE</span>
</span>
+22
View File
@@ -6,6 +6,12 @@ export const site = {
description: env.PUBLIC_SITE_DESCRIPTION || 'Публичная витрина ML-прогнозов для крипторынка.'
};
export type PageMeta = {
title: string;
description: string;
url: string;
};
export function pageTitle(title?: string): string {
return title ? `${title} | ${site.name}` : site.name;
}
@@ -14,3 +20,19 @@ export function canonical(path = '/'): string {
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
return `${site.url}${normalizedPath}`;
}
export function pageMeta({
title,
description = site.description,
path = '/'
}: {
title?: string;
description?: string;
path?: string;
} = {}): PageMeta {
return {
title: pageTitle(title),
description,
url: canonical(path)
};
}
+29
View File
@@ -0,0 +1,29 @@
import { env } from '$env/dynamic/public';
import {
generateChartData,
SYMBOLS,
TIMEFRAMES,
type ChartData,
type TimeframeId
} from '$lib/stores/chartStore';
const supportedModes = ['research-static'] as const;
export type DashboardDataMode = (typeof supportedModes)[number];
export const dashboardDataMode: DashboardDataMode = supportedModes.includes(
env.PUBLIC_DASHBOARD_DATA_MODE as DashboardDataMode
)
? (env.PUBLIC_DASHBOARD_DATA_MODE as DashboardDataMode)
: 'research-static';
export const dashboardDataNotice =
dashboardDataMode === 'research-static'
? 'Исследовательские синтетические данные'
: 'Исследовательские данные';
export function getDashboardChartData(symbolId: string, timeframeId: TimeframeId): ChartData {
return generateChartData(symbolId, timeframeId);
}
export { SYMBOLS, TIMEFRAMES };