chore: prepare production deployment
This commit is contained in:
@@ -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: 'Исторические свечи' },
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -70,10 +70,6 @@
|
||||
<DashboardMockup />
|
||||
</div>
|
||||
|
||||
<!-- Визуальные элементы -->
|
||||
<span
|
||||
class="absolute -top-60 left-0 z-0 h-80 w-full rotate-3 rounded-full bg-primary opacity-30 blur-[200px]"
|
||||
></span>
|
||||
<span class="absolute bottom-35 left-0 z-0 hidden h-px w-full bg-bg-h xl:block"></span>
|
||||
<span class="absolute top-55 left-0 z-0 hidden h-px w-full bg-bg-h xl:block"></span>
|
||||
</section>
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { pageTitle } from '$lib/config/site';
|
||||
|
||||
const title = $derived(
|
||||
page.status === 404
|
||||
? 'Страница не найдена'
|
||||
: page.status >= 500
|
||||
? 'Внутренняя ошибка'
|
||||
: 'Произошла ошибка'
|
||||
);
|
||||
|
||||
const description = $derived(
|
||||
page.status === 404
|
||||
? 'Проверьте адрес или вернитесь к публичному дашборду Flamy Trade.'
|
||||
: 'Запрос не удалось обработать. Попробуйте обновить страницу позже.'
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle('Страница не найдена')}</title>
|
||||
<title>{pageTitle(title)}</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="flex min-h-screen items-center justify-center px-5 pt-16 text-center">
|
||||
@@ -13,10 +28,10 @@
|
||||
Ошибка {page.status}
|
||||
</p>
|
||||
<h1 class="font-display text-[clamp(2.75rem,9vw,6.5rem)] leading-none font-black uppercase">
|
||||
Страница не найдена
|
||||
{title}
|
||||
</h1>
|
||||
<p class="mx-auto mt-5 max-w-lg text-base leading-relaxed text-desc sm:text-lg">
|
||||
Проверьте адрес или вернитесь к публичному дашборду Flamy Trade.
|
||||
{description}
|
||||
</p>
|
||||
<a
|
||||
href="/"
|
||||
|
||||
@@ -2,24 +2,15 @@
|
||||
import './layout.css';
|
||||
import Header from '$lib/components/layout/Header.svelte';
|
||||
import Footer from '$lib/components/layout/Footer.svelte';
|
||||
import { canonical, pageTitle, site } from '$lib/config/site';
|
||||
import { site } from '$lib/config/site';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle()}</title>
|
||||
<meta name="description" content={site.description} />
|
||||
<link rel="canonical" href={canonical('/')} />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content={site.name} />
|
||||
<meta property="og:title" content={pageTitle()} />
|
||||
<meta property="og:description" content={site.description} />
|
||||
<meta property="og:url" content={canonical('/')} />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content={pageTitle()} />
|
||||
<meta name="twitter:description" content={site.description} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="noise"></div>
|
||||
|
||||
+12
-7
@@ -7,16 +7,21 @@
|
||||
import BlogPreview from './(sections)/BlogPreview.svelte';
|
||||
import Cta from './(sections)/Cta.svelte';
|
||||
import { posts } from '$lib/blog/posts';
|
||||
import { canonical, pageTitle, site } from '$lib/config/site';
|
||||
import { pageMeta } from '$lib/config/site';
|
||||
|
||||
const meta = pageMeta();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle()}</title>
|
||||
<meta name="description" content={site.description} />
|
||||
<link rel="canonical" href={canonical('/')} />
|
||||
<meta property="og:title" content={pageTitle()} />
|
||||
<meta property="og:description" content={site.description} />
|
||||
<meta property="og:url" content={canonical('/')} />
|
||||
<title>{meta.title}</title>
|
||||
<meta name="description" content={meta.description} />
|
||||
<link rel="canonical" href={meta.url} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={meta.title} />
|
||||
<meta property="og:description" content={meta.description} />
|
||||
<meta property="og:url" content={meta.url} />
|
||||
<meta name="twitter:title" content={meta.title} />
|
||||
<meta name="twitter:description" content={meta.description} />
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import { about, params } from './_data';
|
||||
import { canonical, pageTitle } from '$lib/config/site';
|
||||
import { pageMeta } from '$lib/config/site';
|
||||
import { scrollFadeUp, scrollRevealLeft } from '$lib/gsap/actions';
|
||||
import { about, params } from './_data';
|
||||
|
||||
const meta = pageMeta({
|
||||
title: 'О проекте',
|
||||
description:
|
||||
'Flamy Trade показывает исследовательские результаты ML-модели для анализа крипторынка.',
|
||||
path: '/about'
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle('О проекте')}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Flamy Trade показывает демонстрационные результаты ML-модели для анализа крипторынка."
|
||||
/>
|
||||
<link rel="canonical" href={canonical('/about')} />
|
||||
<meta property="og:title" content={pageTitle('О проекте')} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Flamy Trade показывает демонстрационные результаты ML-модели для анализа крипторынка."
|
||||
/>
|
||||
<meta property="og:url" content={canonical('/about')} />
|
||||
<title>{meta.title}</title>
|
||||
<meta name="description" content={meta.description} />
|
||||
<link rel="canonical" href={meta.url} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={meta.title} />
|
||||
<meta property="og:description" content={meta.description} />
|
||||
<meta property="og:url" content={meta.url} />
|
||||
<meta name="twitter:title" content={meta.title} />
|
||||
<meta name="twitter:description" content={meta.description} />
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
@@ -39,10 +43,6 @@
|
||||
Публичная витрина результатов ML-модели, обученной на исторических рыночных данных.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<span
|
||||
class="absolute -top-20 -right-20 z-0 h-80 w-full rotate-3 rounded-full bg-primary opacity-30 blur-[200px]"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<section class="grid max-h-full! grid-cols-1 lg:grid-cols-[1fr_420px]">
|
||||
@@ -74,7 +74,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- padding на родителе — не margin/w-full на самом блоке -->
|
||||
<div class="border-b border-b-bg-h px-5 py-12 sm:px-10 sm:py-16">
|
||||
<div
|
||||
class="flex h-[50vh] w-full items-center justify-center rounded-2xl
|
||||
@@ -108,7 +107,7 @@
|
||||
<p
|
||||
class="mb-2 flex items-center gap-2 font-display text-xs font-bold tracking-widest text-primary uppercase"
|
||||
>
|
||||
⚠ Дисклеймер
|
||||
Дисклеймер
|
||||
</p>
|
||||
<p class="text-sm leading-normal text-desc">
|
||||
Материалы являются результатом работы исследовательской ML-модели и используются в
|
||||
|
||||
@@ -19,7 +19,7 @@ export const about: About[] = [
|
||||
desc: 'Назначение',
|
||||
title: 'Исследовательская платформа',
|
||||
text: [
|
||||
'Flamy AI — публичная витрина результатов ML-модели, обученной на исторических рыночных данных. Показывает прогнозы по крипто-инструментам: направление, уровни входа, Take Profit и Stop Loss.',
|
||||
'Flamy Trade — публичная витрина результатов ML-модели, обученной на исторических рыночных данных. Показывает прогнозы по крипто-инструментам: направление, уровни входа, Take Profit и Stop Loss.',
|
||||
'Платформа создана для изучения поведения ML-модели в рыночных условиях — не для торговли реальными средствами.'
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts">
|
||||
import { posts } from '$lib/blog/posts';
|
||||
import { canonical, pageTitle } from '$lib/config/site';
|
||||
import { scrollRevealLeft, scrollFadeUp, scrollStagger } from '$lib/gsap/actions';
|
||||
import { pageMeta } from '$lib/config/site';
|
||||
import { scrollFadeUp, scrollRevealLeft, scrollStagger } from '$lib/gsap/actions';
|
||||
|
||||
const meta = pageMeta({
|
||||
title: 'Блог',
|
||||
description:
|
||||
'Гайды Flamy Trade по чтению графиков, управлению рисками и работе с ML-прогнозами.',
|
||||
path: '/blog'
|
||||
});
|
||||
|
||||
function fmtDate(d: string) {
|
||||
try {
|
||||
@@ -17,18 +24,15 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle('Блог')}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Гайды Flamy Trade по чтению графиков, управлению рисками и работе с ML-прогнозами."
|
||||
/>
|
||||
<link rel="canonical" href={canonical('/blog')} />
|
||||
<meta property="og:title" content={pageTitle('Блог')} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Гайды Flamy Trade по чтению графиков, управлению рисками и работе с ML-прогнозами."
|
||||
/>
|
||||
<meta property="og:url" content={canonical('/blog')} />
|
||||
<title>{meta.title}</title>
|
||||
<meta name="description" content={meta.description} />
|
||||
<link rel="canonical" href={meta.url} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={meta.title} />
|
||||
<meta property="og:description" content={meta.description} />
|
||||
<meta property="og:url" content={meta.url} />
|
||||
<meta name="twitter:title" content={meta.title} />
|
||||
<meta name="twitter:description" content={meta.description} />
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
@@ -47,10 +51,6 @@
|
||||
Гайды по чтению графиков, управлению рисками и работе с ML-прогнозами.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<span
|
||||
class="absolute -right-20 -bottom-40 z-0 h-80 w-full -rotate-3 rounded-full bg-primary opacity-30 blur-[200px]"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -113,7 +113,7 @@
|
||||
<div
|
||||
class="mr-6 shrink-0 pt-1 text-desc/30 transition-[color,translate] duration-700 group-hover:translate-x-2 group-hover:text-primary"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M4 10 H16 M12 6 L16 10 L12 14"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { canonical, pageTitle } from '$lib/config/site';
|
||||
import { pageMeta } from '$lib/config/site';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
const { post } = $derived(data);
|
||||
const meta = $derived(
|
||||
pageMeta({
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
path: `/blog/${post.slug}`
|
||||
})
|
||||
);
|
||||
|
||||
function fmtDate(d: string) {
|
||||
try {
|
||||
@@ -19,16 +26,16 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle(post.title)}</title>
|
||||
<meta name="description" content={post.description} />
|
||||
<link rel="canonical" href={canonical(`/blog/${post.slug}`)} />
|
||||
<title>{meta.title}</title>
|
||||
<meta name="description" content={meta.description} />
|
||||
<link rel="canonical" href={meta.url} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content={pageTitle(post.title)} />
|
||||
<meta property="og:description" content={post.description} />
|
||||
<meta property="og:url" content={canonical(`/blog/${post.slug}`)} />
|
||||
<meta property="og:title" content={meta.title} />
|
||||
<meta property="og:description" content={meta.description} />
|
||||
<meta property="og:url" content={meta.url} />
|
||||
<meta property="article:published_time" content={post.date} />
|
||||
<meta name="twitter:title" content={pageTitle(post.title)} />
|
||||
<meta name="twitter:description" content={post.description} />
|
||||
<meta name="twitter:title" content={meta.title} />
|
||||
<meta name="twitter:description" content={meta.description} />
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
@@ -37,7 +44,7 @@
|
||||
href="/blog"
|
||||
class="back-link mb-6 inline-flex items-center gap-1.5 font-mono text-[0.625rem] tracking-[0.08em] uppercase transition-colors"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M11 7 H3 M6 4 L3 7 L6 10"
|
||||
stroke="currentColor"
|
||||
@@ -63,7 +70,7 @@
|
||||
{/if}
|
||||
|
||||
<h1
|
||||
class="mb-5 font-display text-[clamp(1.875rem,5vw,2.75rem)] leading-[1.08] font-black tracking-[-0.035em] text-title"
|
||||
class="mb-5 font-display text-[clamp(1.875rem,5vw,2.75rem)] leading-[1.08] font-black tracking-tight text-title"
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
@@ -101,7 +108,7 @@
|
||||
href="/blog"
|
||||
class="back-link inline-flex items-center gap-1.5 font-mono text-[0.625rem] tracking-[0.08em] uppercase transition-colors"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M11 7 H3 M6 4 L3 7 L6 10"
|
||||
stroke="currentColor"
|
||||
@@ -118,7 +125,7 @@
|
||||
class="inline-flex items-center gap-2 rounded-xl bg-primary px-5 py-3 font-display text-sm font-normal transition-colors hover:bg-primary-h"
|
||||
>
|
||||
Открыть дашборд
|
||||
<img src="/images/icons/arrow-right.svg" alt="" class="h-4 w-4" />
|
||||
<img src="/images/icons/arrow-right.svg" alt="" aria-hidden="true" class="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,7 +146,7 @@
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
color: var(--color-title);
|
||||
letter-spacing: -0.025em;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.2;
|
||||
margin-top: 2.25em;
|
||||
margin-bottom: 0.75em;
|
||||
@@ -149,7 +156,7 @@
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-title);
|
||||
letter-spacing: -0.02em;
|
||||
letter-spacing: 0;
|
||||
margin-top: 1.75em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getPostBySlug } from '$lib/blog/posts';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { gsap } from 'gsap';
|
||||
import { generateChartData, SYMBOLS, TIMEFRAMES, type TimeframeId } from '$lib/stores/chartStore';
|
||||
import Chart from '$lib/components/Chart.svelte';
|
||||
import { canonical, pageTitle } from '$lib/config/site';
|
||||
import {
|
||||
dashboardDataNotice,
|
||||
getDashboardChartData,
|
||||
SYMBOLS,
|
||||
TIMEFRAMES
|
||||
} from '$lib/data/dashboard';
|
||||
import type { TimeframeId } from '$lib/stores/chartStore';
|
||||
import { pageMeta } from '$lib/config/site';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
const meta = pageMeta({
|
||||
title: 'Дашборд',
|
||||
description:
|
||||
'Публичный исследовательский дашборд Flamy Trade с графиком, ML-прогнозом и уровнями Entry, TP и SL.',
|
||||
path: '/dashboard'
|
||||
});
|
||||
|
||||
let activeSymbol = $state('BTCUSDT');
|
||||
let activeTimeframe = $state<TimeframeId>('5m');
|
||||
let chartHeight = $state(520);
|
||||
|
||||
const sym = $derived(SYMBOLS.find((s) => s.id === activeSymbol) ?? SYMBOLS[0]);
|
||||
const data = $derived(generateChartData(activeSymbol, activeTimeframe));
|
||||
const data = $derived(getDashboardChartData(activeSymbol, activeTimeframe));
|
||||
|
||||
let titleEl = $state<HTMLElement>(null!);
|
||||
let panelEl = $state<HTMLElement>(null!);
|
||||
@@ -66,18 +79,15 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{pageTitle('Дашборд')}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Демонстрационный дашборд Flamy Trade с графиком, ML-прогнозом и уровнями Entry, TP и SL."
|
||||
/>
|
||||
<link rel="canonical" href={canonical('/dashboard')} />
|
||||
<meta property="og:title" content={pageTitle('Дашборд')} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Демонстрационный дашборд Flamy Trade с графиком, ML-прогнозом и уровнями Entry, TP и SL."
|
||||
/>
|
||||
<meta property="og:url" content={canonical('/dashboard')} />
|
||||
<title>{meta.title}</title>
|
||||
<meta name="description" content={meta.description} />
|
||||
<link rel="canonical" href={meta.url} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={meta.title} />
|
||||
<meta property="og:description" content={meta.description} />
|
||||
<meta property="og:url" content={meta.url} />
|
||||
<meta name="twitter:title" content={meta.title} />
|
||||
<meta name="twitter:description" content={meta.description} />
|
||||
</svelte:head>
|
||||
|
||||
<main class="min-h-screen bg-bg">
|
||||
@@ -89,7 +99,7 @@
|
||||
Дашборд
|
||||
</h1>
|
||||
<p class="mt-1.5 font-mono text-[0.625rem] tracking-widest text-desc uppercase sm:text-xs">
|
||||
ML-прогноз · Горизонт 19 свечей · Таймфрейм 5M
|
||||
{dashboardDataNotice} · Горизонт 19 свечей · Таймфрейм {activeTimeframe.toUpperCase()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -112,6 +122,7 @@
|
||||
: 'border-transparent text-desc hover:text-title'
|
||||
)}
|
||||
onclick={() => (activeSymbol = s.id)}
|
||||
aria-pressed={activeSymbol === s.id}
|
||||
>
|
||||
{s.short}
|
||||
</button>
|
||||
@@ -126,6 +137,7 @@
|
||||
activeTimeframe === tf.id ? 'bg-bg-h text-title' : 'text-desc hover:text-title'
|
||||
)}
|
||||
onclick={() => (activeTimeframe = tf.id)}
|
||||
aria-pressed={activeTimeframe === tf.id}
|
||||
>
|
||||
{tf.label}
|
||||
</button>
|
||||
@@ -177,7 +189,14 @@
|
||||
>
|
||||
Уверенность
|
||||
</span>
|
||||
<div class="h-1.5 w-16 overflow-hidden rounded-full bg-bg-h sm:w-20">
|
||||
<div
|
||||
class="h-1.5 w-16 overflow-hidden rounded-full bg-bg-h sm:w-20"
|
||||
role="progressbar"
|
||||
aria-label="Уверенность прогноза"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow={data.signal.confidence}
|
||||
>
|
||||
<div
|
||||
class={cn(
|
||||
'h-full rounded-full transition-all duration-700',
|
||||
@@ -251,14 +270,9 @@
|
||||
</div>
|
||||
|
||||
<span class="ml-auto font-mono text-[0.5rem] tracking-widest text-desc/60 uppercase">
|
||||
не является инвестиционной рекомендацией
|
||||
исследовательские данные · не является инвестиционной рекомендацией
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="pointer-events-none fixed top-0 left-1/2 h-80 w-full max-w-3xl -translate-x-1/2 rounded-full bg-primary opacity-[0.07] blur-[160px]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { canonical } from '$lib/config/site';
|
||||
|
||||
export const GET = () => {
|
||||
const body = `User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: ${canonical('/sitemap.xml')}
|
||||
`;
|
||||
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
'content-type': 'text/plain; charset=utf-8',
|
||||
'cache-control': 'public, max-age=3600'
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user