Initial commit: Astro frontend project

This commit is contained in:
Web-serfer 2026-03-26 04:05:46 +05:00
commit dcca6b154a
104 changed files with 5827 additions and 0 deletions

36
.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# Dependency directories
node_modules/
frontend/node_modules/
backend/node_modules/
# Build outputs
dist/
frontend/dist/
backend/dist/
*.log
# Environment variables
.env
.env.local
.env.production
# OS generated files
.DS_Store
Thumbs.db
# IDE files
.idea/
.vscode/
*.iml
# Qwen assistant files
.qwen/
# Backend (PocketBase) - не включается в репозиторий
backend/
# Executable files
*.exe
# Lock files (генерируются автоматически)
# bun.lock

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
20

61
QWEN.md Normal file
View file

@ -0,0 +1,61 @@
# Правила взаимодействия с Qwen Code Assistant
## Основные принципы
1. **Изменения в коде возможны только с явного разрешения пользователя**
- Перед внесением любых изменений в файлы ассистент должен получить подтверждение от пользователя
- Все изменения должны быть предварительно объяснены пользователю
- Перед решением конкретной задачи всегда составлять план
- После внесения изменений в код - проводить проверку - только после этого приступать к дальнейшему решению задачи
2. **Прозрачность действий**
- Ассистент должен объяснить, какие изменения планируется внести
- Необходимо указать, в какие файлы будут внесены изменения
- Следует объяснить последствия предполагаемых изменений
3. **Безопасность кода**
- Все изменения должны проходить проверку на безопасность
- Не должны вноситься изменения, которые могут повредить функциональность приложения
- Рекомендуется создание резервных копий при значительных изменениях
4. **Согласование архитектурных решений**
- При внесении изменений, затрагивающих архитектуру приложения, необходима дискуссия с пользователем
- Предложения по улучшению архитектуры должны обсуждаться до реализации
5. **Работа с разными типами проектов**
- Уважать существующую архитектуру и стиль кода проекта
- Следовать установленным в проекте принципам и паттернам
6. **Использование Bun**
- Все команды должны выполняться с использованием Bun (bun install, bun dev, bun build и т.д.)
- При создании скриптов в package.json, они должны быть совместимы с Bun
7. **Язык общения**
- Всё общение с пользователем происходит на русском языке
8. **Проверка изменений**
- После внесения изменений в код не требуется запускать сервер разработки для проверки
- Пользователь самостоятельно запускает сервер и проверяет изменения
9. **Проверка типов данных**
- Проверять проект на ошибки типизации через команду `bun run tsc --noEmit -p frontend/tsconfig.json`
- В проекте не должно быть типов any
- Все интерфейсы компонентов прописывать в файле globalInterfaces.ts
- При работе с PocketBase использовать актуальные сигнатуры методов из файла `D:\Verstka\production\astro_minivan\frontend\node_modules\pocketbase\dist\pocketbase.es.d.ts`
10. **Плагин @astrojs/sitemap**
- Обязательно к установке в проект пакета @astrojs/sitemap
- Обязательно к созданию в проекте файла .nvmrc
11. **Замена хоста при развертывании проекта**
- Обязательно нужно изменить http://localhost:3000/ в шаблонах писем на реальный
## Qwen Added Memories
- URL документации Astro: https://docs.astro.build/en/getting-started/
- URL документации PocketBase: https://pocketbase.io/docs/
- URL документации SolidJS: https://docs.solidjs.com/solid-start/getting-started
- URL документации astro-icons: https://www.astroicon.dev/getting-started/
- URL документации PayloadCMS: https://payloadcms.com/docs/getting-started/what-is-payload
- URL документации Prisma ORM и Astro: https://www.prisma.io/docs/ai/prompts/astro

62
README.md Normal file
View file

@ -0,0 +1,62 @@
# Astro REDi Monorepo
Этот монорепозиторий содержит frontend и backend части приложения Astro REDi.
## Структура проекта
- `frontend/` - Astro-приложение для фронтенда
- `backend/` - PocketBase-приложение для бэкенда
## Установка
1. Установите зависимости для всего проекта:
```bash
bun install
```
2. Установите зависимости для frontend:
```bash
cd frontend && bun install
```
Или выполните установку всех зависимостей одной командой:
```bash
bun run install:all
```
## Запуск приложения
Для одновременного запуска frontend и backend приложения выполните:
```bash
bun run dev
```
Для запуска только frontend:
```bash
bun run dev:frontend
```
Для запуска только backend:
```bash
bun run dev:backend
```
Для остановки запущенных процессов:
```bash
bun run stop
```
## Сборка проекта
Для сборки frontend приложения:
```bash
bun run build
```

70
bun.lock Normal file
View file

@ -0,0 +1,70 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "astro-redi-monorepo",
"devDependencies": {
"concurrently": "^8.2.0",
},
},
},
"packages": {
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concurrently": ["concurrently@8.2.2", "", { "dependencies": { "chalk": "^4.1.2", "date-fns": "^2.30.0", "lodash": "^4.17.21", "rxjs": "^7.8.1", "shell-quote": "^1.8.1", "spawn-command": "0.0.2", "supports-color": "^8.1.1", "tree-kill": "^1.2.2", "yargs": "^17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg=="],
"date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"spawn-command": ["spawn-command@0.0.2", "", {}, "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
}
}

12
frontend/.editorconfig Normal file
View file

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

24
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

1
frontend/.nvmrc Normal file
View file

@ -0,0 +1 @@
20

14
frontend/README.md Normal file
View file

@ -0,0 +1,14 @@
# Redi.dev: Создание современных сайтов на Astro и Next.js
Добро пожаловать на Redi.dev ваш ресурс для изучения и создания высокопроизводительных веб-сайтов с использованием передовых фреймворков Astro и Next.js. Здесь вы найдете статьи, руководства и примеры проектов, посвященные разработке современных, быстрых и SEO-оптимизированных решений.
Мы фокусируемся на технологиях, которые позволяют создавать исключительный пользовательский опыт и обеспечивать максимальную производительность.
<img style="max-width:500px" src="public/assets/images/preview.jpg">
На Redi.dev мы делимся опытом в:
- **Astro:** Создание контент-ориентированных сайтов с молниеносной загрузкой благодаря уникальной "островной" архитектуре.
- **Next.js:** Разработка мощных и масштабируемых веб-приложений с серверным рендерингом и статической генерацией.
Присоединяйтесь к нам, чтобы освоить лучшие практики современной веб-разработки!

23
frontend/astro.config.mjs Normal file
View file

@ -0,0 +1,23 @@
import { defineConfig } from "astro/config";
import solidJs from '@astrojs/solid-js';
import tailwind from "@astrojs/tailwind";
import icon from "astro-icon";
import sitemap from "@astrojs/sitemap";
import node from "@astrojs/node";
// https://astro.build/config
export default defineConfig({
site: process.env.PUBLIC_SITE_URL || "https://hts.ru", // Заменить на реальный домен при развертывании
integrations: [tailwind(), solidJs(), icon(), sitemap()],
output: "server",
vite: {
build: {
// Встраивать CSS размером до 15 КБ
cssInlineLimit: 15000
}
},
adapter: node({
mode: "standalone",
}),
});

1299
frontend/bun.lock Normal file

File diff suppressed because it is too large Load diff

39
frontend/package.json Normal file
View file

@ -0,0 +1,39 @@
{
"name": "redi-dev",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro preview",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"format": "biome format --write .",
"lint": "biome lint .",
"lint:fix": "biome check --apply ."
},
"devDependencies": {
"@astrojs/check": "0.9.8",
"@astrojs/tailwind": "^6.0.2",
"@biomejs/biome": "1.7.3",
"@tailwindcss/typography": "^0.5.13",
"astro": "6.0.8",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5"
},
"packageManager": "bun@1.1.29",
"dependencies": {
"@astrojs/node": "10.0.3",
"@astrojs/sitemap": "3.7.1",
"@astrojs/solid-js": "6.0.1",
"@nanostores/solid": "^1.1.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.11",
"astro-icon": "^1.1.5",
"nanostores": "^1.0.1",
"pocketbase": "^0.26.5",
"sharp": "^0.34.3",
"solid-icons": "^1.1.0",
"solid-js": "^1.9.9"
}
}

View file

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<text y=".9em" font-size="90">-HTS-</text>
</svg>

After

Width:  |  Height:  |  Size: 114 B

BIN
frontend/public/logo.avif Normal file

Binary file not shown.

View file

@ -0,0 +1,2 @@
User-agent: *
Allow: /

View file

@ -0,0 +1,127 @@
article.prose h2 {
text-align: center !important; /* Center on mobile */
}
@media (min-width: 768px) {
/* md breakpoint */
article.prose h2 {
text-align: left !important; /* Left-align on md and above */
}
}
.prose img {
border-radius: 30px;
}
/* Styles for paragraph spacing on mobile */
article.prose p {
margin-top: 1.5em !important; /* Increased top margin for paragraphs on mobile */
margin-bottom: 1.5em !important; /* Increased bottom margin for paragraphs on mobile */
font-size: 0.9em !important; /* Smaller font size for paragraphs on mobile */
}
@media (min-width: 768px) {
/* md breakpoint */
article.prose p {
/* Reset or keep default prose spacing and font size for larger screens */
margin-top: 1em !important; /* Adjust as needed, or remove !important if prose default is fine */
margin-bottom: 1em !important; /* Adjust as needed, or remove !important if prose default is fine */
font-size: 1em !important; /* Revert to default or desired font size for larger screens */
}
}
/* General text readability improvements for article content */
article.prose p {
line-height: 1.75; /* Relaxed line height */
margin-top: 1em; /* Default paragraph spacing */
margin-bottom: 1em;
}
article.prose ul,
article.prose ol {
margin-top: 1em;
margin-bottom: 1em;
}
article.prose li {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
article.prose h1 {
margin-top: 3rem; /* Equivalent to spacing.12 */
margin-bottom: 1.5rem; /* Equivalent to spacing.6 */
}
article.prose h2 {
margin-top: 2.5rem; /* Equivalent to spacing.10 */
margin-bottom: 1.25rem; /* Equivalent to spacing.5 */
}
article.prose h3 {
margin-top: 2rem; /* Equivalent to spacing.8 */
margin-bottom: 1rem; /* Equivalent to spacing.4 */
}
article.prose h4 {
margin-top: 1.5rem; /* Equivalent to spacing.6 */
margin-bottom: 0.75rem; /* Equivalent to spacing.3 */
}
/* Code block styling */
article.prose pre {
background-color: #1a202c; /* neutral-900 */
border: 1px solid #2d3748; /* neutral-800 */
border-radius: 0.5rem; /* lg */
padding: 1rem; /* spacing.4 */
overflow-x: auto; /* Enable horizontal scrolling for long lines */
}
article.prose code {
background-color: #2d3748; /* neutral-800 */
padding: 0.2em 0.4em;
border-radius: 0.25rem;
font-size: 0.875em; /* Smaller font size for inline code */
}
/* Table styling */
article.prose table {
width: 100%;
border-collapse: collapse;
margin-top: 1.5em;
margin-bottom: 1.5em;
}
article.prose th,
article.prose td {
border: 1px solid #4a5568; /* neutral-600 */
padding: 0.75em;
text-align: left;
}
article.prose th {
background-color: #2d3748; /* neutral-800 */
font-weight: bold;
}
/* Link hover effect */
article.prose a {
transition: color 0.2s ease-in-out, text-decoration-color 0.2s ease-in-out;
}
article.prose a:hover {
color: #6366f1; /* indigo-500 */
text-decoration-color: #6366f1; /* indigo-500 */
}
/* Blockquote styling */
article.prose blockquote {
border-left: 4px solid #4f46e5; /* indigo-600 */
padding-left: 1em;
margin-left: 0;
font-style: italic;
color: #a0aec0; /* neutral-400 */
background-color: #1a202c; /* neutral-900 */
padding: 1em;
border-radius: 0.5rem;
}

View file

@ -0,0 +1,37 @@
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Noto+Sans:wght@400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Оборачиваем в @layer base, чтобы Tailwind корректно это обработал */
@layer base {
body {
font-family: 'Noto Sans', sans-serif;
@apply bg-background-light dark:bg-background-dark text-[#181611] dark:text-white overflow-x-hidden;
}
h1, h2, h3, h4, h5, h6, .font-display {
font-family: 'Space Grotesk', sans-serif;
}
}
/* src/assets/css/global.css */
.hamburger-line {
@apply h-[2px] w-8 bg-white transition-all duration-300 ease-in-out;
}
/* Состояние когда меню открыто (.is-active) */
.is-active .line-1 {
transform: translateY(10px) rotate(45deg);
}
.is-active .line-2 {
opacity: 0;
transform: translateX(-20px);
}
.is-active .line-3 {
transform: translateY(-10px) rotate(-45deg);
}

Binary file not shown.

View file

@ -0,0 +1,300 @@
---
import PageHeading from '@components/base/PageHeading.astro'
const skills = [
"Astro",
"NextJS",
"SolidJS",
"Preact",
"React",
"SolidJS",
"TypeScript",
"Tailwind CSS"
]
---
<section class="space-y-8 md:space-y-12">
<PageHeading
title="Информация и профессиональный опыт"
description="Привет 👋 Я Redi, Jamstack-разработчик. Создаю современные веб-приложения, увлекаюсь новыми технологиями и делюсь опыт через статьи и open-source проекты."
/>
<!-- Первая строка: изображение + блок доступности + текст -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-8 mb-8 md:mb-12">
<!-- Левая колонка - 2 ячейки -->
<div class="lg:col-span-1 space-y-4 md:space-y-6">
<!-- Картинка -->
<div class="relative group">
<div class="relative overflow-hidden rounded-xl md:rounded-2xl shadow-lg md:shadow-2xl">
<img
src="/images/about.avif"
width={280}
height={280}
alt="RediBedi - Веб-разработчик"
class="w-full h-auto object-cover transition-transform duration-700 group-hover:scale-105"
loading="eager"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</div>
</div>
<!-- Блок "Доступен для проектов" с анимированной ракетой -->
<div class="p-4 md:p-6 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-xl md:rounded-2xl shadow-md md:shadow-lg overflow-hidden relative rocket-container">
<div class="text-center relative z-10">
<div class="rocket-animation">
<div class="rocket text-2xl md:text-4xl">🚀</div>
<div class="exhaust"></div>
<div class="stars">
{Array.from({ length: 10 }).map((_, i) => (
<div class={`star star-${i + 1}`}></div>
))}
</div>
</div>
<h3 class="text-base md:text-lg font-semibold text-white mb-1 md:mb-2 mt-2 md:mt-4">Доступен для проектов</h3>
<p class="text-indigo-100 text-xs md:text-sm">Готов к новым вызовам и интересным проектам</p>
</div>
</div>
</div>
<!-- Правая колонка - Текст -->
<div class="lg:col-span-2">
<div class="p-4 md:p-6 lg:p-8 bg-white dark:bg-neutral-900 rounded-xl md:rounded-2xl shadow-md md:shadow-lg border border-gray-100 dark:border-neutral-800 h-full">
<p class="text-md md:text-base text-center md:text-left lg:text-lg leading-7 md:leading-8 text-gray-700 dark:text-neutral-300">
Jamstack-разработчик с 12+ годами опыта, специализируюсь на создании высоконагруженных веб-приложений.
Люблю работать с современным стеком технологий и решать сложные архитектурные задачи.
Постоянно изучаю новые подходы к разработке и делюсь знаниями с сообществом через технические статьи
и open-source проекты. Верю в силу сообщества и открытого кода.
</p>
</div>
</div>
</div>
<!-- Вторая строка: Технологии на всю ширину -->
<div>
<h2 class="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-4 md:mb-6 lg:mb-8 text-center">
Технологии которые я использую
</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2 md:gap-3 lg:gap-4">
{skills.map((skill, index) => (
<div
class="flex items-center justify-center p-2 md:p-3 lg:p-4 bg-gradient-to-br from-white to-gray-50 dark:from-neutral-800 dark:to-neutral-700 rounded-lg md:rounded-xl shadow-sm hover:shadow-md transition-all duration-300 hover:-translate-y-1 border border-gray-200 dark:border-neutral-600 group"
style={`animation-delay: ${index * 50}ms`}
>
<span class="text-xs md:text-sm font-medium text-gray-700 dark:text-neutral-200 text-center group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
{skill}
</span>
</div>
))}
</div>
</div>
</section>
<style>
/* Анимация для карточек технологий */
.grid.grid-cols-2 > div {
animation: fadeInUp 0.6s ease-out forwards;
opacity: 0;
transform: translateY(20px);
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
/* Плавные переходы */
.group {
transition: all 0.3s ease-in-out;
}
/* Анимация ракеты */
.rocket-container {
overflow: hidden;
}
.rocket-animation {
position: relative;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
}
@media (min-width: 768px) {
.rocket-animation {
height: 80px;
}
}
.rocket {
animation: rocketFloat 3s ease-in-out infinite;
z-index: 2;
position: relative;
}
.exhaust {
position: absolute;
bottom: -8px;
width: 16px;
height: 24px;
background: linear-gradient(to top, #ff9d00, #ff2c00, transparent);
border-radius: 50% 50% 0 0;
opacity: 0.8;
animation: exhaustPulse 1.5s ease-in-out infinite;
z-index: 1;
filter: blur(2px);
}
@media (min-width: 768px) {
.exhaust {
bottom: -10px;
width: 20px;
height: 30px;
filter: blur(3px);
}
}
.stars {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.star {
position: absolute;
background-color: white;
border-radius: 50%;
opacity: 0;
}
/* Создаем несколько звездочек с разными позициями и анимациями */
.star-1 { width: 1px; height: 1px; top: 20%; left: 30%; animation: starShine 3s 0.5s infinite; }
.star-2 { width: 2px; height: 2px; top: 40%; left: 60%; animation: starShine 4s 1s infinite; }
.star-3 { width: 1px; height: 1px; top: 60%; left: 20%; animation: starShine 3.5s 0.8s infinite; }
.star-4 { width: 2px; height: 2px; top: 30%; left: 80%; animation: starShine 4.5s 1.2s infinite; }
.star-5 { width: 1px; height: 1px; top: 70%; left: 40%; animation: starShine 3.2s 0.6s infinite; }
.star-6 { width: 2px; height: 2px; top: 10%; left: 50%; animation: starShine 4.2s 1.4s infinite; }
.star-7 { width: 1px; height: 1px; top: 50%; left: 10%; animation: starShine 3.8s 0.9s infinite; }
.star-8 { width: 2px; height: 2px; top: 80%; left: 70%; animation: starShine 4.8s 1.6s infinite; }
.star-9 { width: 1px; height: 1px; top: 25%; left: 90%; animation: starShine 3.3s 1.1s infinite; }
.star-10 { width: 2px; height: 2px; top: 65%; left: 30%; animation: starShine 4.3s 0.7s infinite; }
@media (min-width: 768px) {
.star-1, .star-3, .star-5, .star-7, .star-9 { width: 2px; height: 2px; }
.star-2, .star-4, .star-6, .star-8, .star-10 { width: 3px; height: 3px; }
}
@keyframes rocketFloat {
0%, 100% {
transform: translateY(0) rotate(0deg);
}
25% {
transform: translateY(-4px) rotate(2deg);
}
50% {
transform: translateY(0) rotate(0deg);
}
75% {
transform: translateY(4px) rotate(-2deg);
}
}
@keyframes exhaustPulse {
0%, 100% {
height: 16px;
opacity: 0.6;
}
50% {
height: 24px;
opacity: 0.9;
}
}
@media (min-width: 768px) {
@keyframes exhaustPulse {
0%, 100% {
height: 20px;
opacity: 0.6;
}
50% {
height: 30px;
opacity: 0.9;
}
}
}
@keyframes starShine {
0%, 100% {
opacity: 0;
transform: scale(0.5);
}
50% {
opacity: 1;
transform: scale(1);
}
}
/* Эффект при наведении на контейнер */
.rocket-container:hover .rocket {
animation: rocketHover 2s ease-in-out;
}
.rocket-container:hover .exhaust {
animation: exhaustBoost 1s ease-in-out;
}
@keyframes rocketHover {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0);
}
}
@media (min-width: 768px) {
@keyframes rocketHover {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-15px);
}
100% {
transform: translateY(0);
}
}
}
@keyframes exhaustBoost {
0%, 100% {
height: 16px;
opacity: 0.6;
}
50% {
height: 32px;
opacity: 1;
}
}
@media (min-width: 768px) {
@keyframes exhaustBoost {
0%, 100% {
height: 20px;
opacity: 0.6;
}
50% {
height: 40px;
opacity: 1;
}
}
}
</style>

View file

@ -0,0 +1,69 @@
---
import { Icon } from 'astro-icon/components'
---
<section class="py-8 md:py-12 bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-neutral-900 dark:to-neutral-800 rounded-xl md:rounded-2xl">
<div class="max-w-4xl mx-auto px-4 sm:px-5 lg:px-8">
<div class="text-center">
<!-- Заголовок -->
<h2 class="text-2xl md:text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white mb-4 md:mb-6">
Готовы начать проект?
</h2>
<!-- Описание -->
<p class="text-base md:text-lg lg:text-xl text-gray-600 dark:text-neutral-300 mb-6 md:mb-8 lg:mb-10 max-w-2xl mx-auto">
Обсудим ваши идеи и создадим что-то amazing вместе. Я всегда на связи и готов помочь с реализацией вашего проекта.
</p>
<!-- Кнопки контактов -->
<div class="flex flex-col sm:flex-row gap-3 md:gap-4 justify-center items-center">
<!-- WhatsApp -->
<a
href="https://wa.me/79222538375?text=Здравствуйте!%20Хочу%20обсудить%20проект"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center px-5 py-3 md:px-6 md:py-3 lg:px-8 lg:py-4 text-sm md:text-base font-semibold rounded-xl md:rounded-2xl shadow-md md:shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-0.5 md:hover:-translate-y-1 bg-green-500 hover:bg-green-600 text-white contact-cta-button w-full sm:w-auto justify-center"
>
<Icon name="whatsapp" class="w-5 h-5 md:w-6 md:h-6 mr-2 md:mr-3" />
Написать в WhatsApp
</a>
<!-- Email -->
<a
href="mailto:redibedi2019@gmail.com"
class="inline-flex items-center px-5 py-3 md:px-6 md:py-3 lg:px-8 lg:py-4 text-sm md:text-base font-semibold rounded-xl md:rounded-2xl shadow-md md:shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-0.5 md:hover:-translate-y-1 bg-white dark:bg-neutral-800 text-gray-700 dark:text-neutral-300 border border-gray-200 dark:border-neutral-700 contact-cta-button w-full sm:w-auto justify-center"
>
<Icon name="envelope" class="w-4 h-4 md:w-5 md:h-5 mr-2" />
<span class="truncate">redibedi2019@gmail.com</span>
</a>
</div>
<!-- Дополнительная информация -->
<div class="mt-6 md:mt-8 p-3 md:p-4 bg-white dark:bg-neutral-800 rounded-xl md:rounded-2xl shadow-sm">
<p class="text-xs md:text-sm text-gray-500 dark:text-neutral-400">
⚡ Обычно отвечаю в течение 1-2 часов в рабочее время
</p>
</div>
</div>
</div>
</section>
<style>
/* Уникальные стили для компонента ContactCTA */
.contact-cta-button {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.contact-cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* Адаптивность для мобильных устройств */
@media (max-width: 640px) {
.contact-cta-button {
padding: 0.75rem 1.25rem;
font-size: 0.875rem;
}
}
</style>

View file

@ -0,0 +1,31 @@
---
export interface Breadcrumb {
label: string;
url?: string;
}
interface Props {
breadcrumbs: Breadcrumb[];
}
const { breadcrumbs }: Props = Astro.props;
---
<div class="w-full max-w-[1440px] mx-auto px-4 sm:px-10 py-6">
<div class="flex flex-wrap gap-2 items-center">
{breadcrumbs.map((crumb, index) => (
<span key={index} class="flex items-center">
{crumb.url ? (
<a class="text-[#bab09c] hover:text-white transition-colors text-sm font-medium uppercase" href={crumb.url}>
{crumb.label}
</a>
) : (
<span class="text-white text-sm font-bold uppercase">{crumb.label}</span>
)}
{index < breadcrumbs.length - 1 && (
<span class="text-[#bab09c] text-sm mx-2">/</span>
)}
</span>
))}
</div>
</div>

View file

@ -0,0 +1,37 @@
---
interface Props {
href?: string;
variant?: 'primary' | 'outline' | 'dark';
className?: string; // Доп. классы если нужны
[x: string]: any; // Остальные атрибуты (type, onclick и т.д.)
}
const { href, variant = 'primary', className = '', ...rest } = Astro.props;
// Определение HTML тега: если есть href, то <a>, иначе <button>
const Tag = href ? 'a' : 'button';
// Базовые стили для всех кнопок (размер, шрифт, анимации)
const baseStyles = "inline-flex items-center justify-center h-14 px-8 font-bold uppercase tracking-wider transition-all rounded duration-300 ease-out whitespace-nowrap cursor-pointer";
// Варианты стилей
const variants = {
// Оранжевая кнопка (как в хедере и на главном экране)
primary: "bg-primary hover:bg-primary-hover text-background-dark shadow-[0_0_15px_rgba(242,166,13,0.3)] hover:shadow-[0_0_20px_rgba(242,166,13,0.5)] hover:-translate-y-[2px]",
// Прозрачная с обводкой (для Hero)
outline: "border border-white/30 hover:border-white bg-white/5 hover:bg-white/10 text-white backdrop-blur-sm",
// Темная кнопка (для оранжевого блока CTA)
dark: "bg-background-dark hover:bg-black text-white shadow-xl hover:shadow-2xl"
};
---
<Tag
href={href}
class:list={[baseStyles, variants[variant], className]}
{...rest}
>
<slot />
{/* Слот позволяет вставлять и текст, и иконки внутрь кнопки */}
</Tag>

View file

@ -0,0 +1,53 @@
---
export interface Props {
title: string;
subtitle: string;
categoryName?: string;
categoryIcon?: string;
placeholder?: string;
gradientWord?: string;
}
const {
title,
subtitle,
categoryName = "Корпоративные новости",
categoryIcon = "campaign",
placeholder = "Поиск по новостям...",
gradientWord = "компании"
} = Astro.props;
---
<!-- Hero Section -->
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full px-4 md:px-10 py-8 md:pb-12">
<div class="flex flex-col md:flex-row justify-between items-start md:items-end gap-6 border-b border-surface-border pb-8">
<div class="flex flex-col gap-4 max-w-2xl">
<div class="flex items-center gap-2 text-primary font-mono text-sm uppercase tracking-wider">
<span class="material-symbols-outlined text-lg">{categoryIcon}</span>
<span>{categoryName}</span>
</div>
<h1 class="text-white text-4xl md:text-5xl lg:text-6xl font-black leading-tight tracking-tight uppercase whitespace-nowrap">
<span>{title} </span>
<span class="text-transparent bg-clip-text bg-gradient-to-r from-primary to-orange-400">{gradientWord}</span>
</h1>
<p class="text-gray-400 text-lg max-w-xl font-light">
{subtitle}
</p>
</div>
<!-- Search -->
<div class="w-full md:w-auto md:min-w-[320px]">
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="material-symbols-outlined text-gray-500 group-focus-within:text-primary">search</span>
</div>
<input
class="block w-full pl-10 pr-3 py-3 border border-surface-border rounded bg-surface-dark text-white placeholder-gray-500 focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary sm:text-sm transition-all"
placeholder={placeholder}
type="text"
/>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,16 @@
---
const { title, description } = Astro.props
---
<div class="relative z-20 w-full mx-auto lg:mx-0">
<h1
class="text-2xl font-bold text-center tracking-tight text-neutral-900 dark:text-neutral-100 sm:text-3xl lg:text-4xl"
>
{title}
</h1>
<p
class="mt-3 text-sm leading-6 text-center text-neutral-600 dark:text-neutral-400 sm:mt-4 lg:mt-6 sm:leading-7 lg:leading-8 sm:text-base lg:text-lg"
>
{description}
</p>
</div>

View file

@ -0,0 +1,65 @@
import type { Component } from 'solid-js';
import { Show } from 'solid-js';
import { FiChevronLeft, FiChevronRight } from 'solid-icons/fi';
export interface PaginationProps {
currentPage: number;
lastPage: number;
url: {
prev?: string;
next?: string;
};
}
interface Props {
page: PaginationProps;
}
const Pagination: Component<Props> = (props) => {
const isPrevDisabled = () => !props.page.url.prev;
const isNextDisabled = () => !props.page.url.next;
return (
<Show when={props.page.lastPage > 1}>
<nav
class="mt-12 flex items-center justify-center gap-6 border-t border-neutral-200 pt-8 dark:border-neutral-800"
aria-label="Навигация по страницам"
>
{/* Кнопка "Назад" */}
<a
href={props.page.url.prev}
classList={{
'flex items-center justify-center h-10 w-10 rounded-full border transition-all duration-300': true,
'border-neutral-300 bg-neutral-100 text-neutral-400 cursor-not-allowed dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-600': isPrevDisabled(),
'border-neutral-300 bg-white text-neutral-700 shadow-sm hover:bg-neutral-50 hover:shadow-md active:scale-95 dark:bg-neutral-800 dark:border-neutral-600 dark:text-neutral-300 dark:hover:bg-neutral-700': !isPrevDisabled(),
}}
aria-disabled={isPrevDisabled()}
aria-label="Перейти на предыдущую страницу"
>
<FiChevronLeft class="h-5 w-5" />
</a>
{/* Индикатор текущей страницы */}
<span class="text-sm font-medium text-neutral-600 dark:text-neutral-400">
Страница <span class="font-semibold text-blue-600 dark:text-blue-400">{props.page.currentPage}</span> из {props.page.lastPage}
</span>
{/* Кнопка "Вперед" */}
<a
href={props.page.url.next}
classList={{
'flex items-center justify-center h-10 w-10 rounded-full border transition-all duration-300': true,
'border-neutral-300 bg-neutral-100 text-neutral-400 cursor-not-allowed dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-600': isNextDisabled(),
'border-neutral-300 bg-white text-neutral-700 shadow-sm hover:bg-neutral-50 hover:shadow-md active:scale-95 dark:bg-neutral-800 dark:border-neutral-600 dark:text-neutral-300 dark:hover:bg-neutral-700': !isNextDisabled(),
}}
aria-disabled={isNextDisabled()}
aria-label="Перейти на следующую страницу"
>
<FiChevronRight class="h-5 w-5" />
</a>
</nav>
</Show>
);
};
export default Pagination;

View file

@ -0,0 +1,93 @@
---
interface Post {
title: string;
slug: string;
description: string;
publishDate: string;
tags?: string[];
}
interface Props {
posts: Post[];
}
const { posts } = Astro.props;
function formatDate(date: string): string {
if (!date) return '';
return new Date(date).toLocaleString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
---
{
posts.map((post) => {
const postLink = `/blog/${post.slug}`;
const formattedDate = formatDate(post.publishDate);
const displayTitle = post.title.replace('{year}', new Date().getFullYear().toString());
return (
<div class="relative p-6 sm:p-7 rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-950 hover:shadow-lg hover:border-neutral-300 dark:hover:border-neutral-700 transition-all duration-300 group">
{/*
1. ГЛАВНАЯ ССЫЛКА (на всю карточку)
Абсолютное позиционирование, самый нижний слой (z-0)
*/}
<a href={postLink} class="absolute inset-0 z-0" aria-label={`Читать: ${displayTitle}`}></a>
{/*
2. КОНТЕНТ
Слой выше (z-10), пропускает клики (pointer-events-none)
*/}
<div class="relative z-10 pointer-events-none flex flex-col h-full">
{/* Заголовок */}
<h2 class="text-xl font-bold text-neutral-900 dark:text-neutral-100 mb-3 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
{displayTitle}
</h2>
{/* Описание */}
<p class="text-sm text-neutral-600 dark:text-neutral-400 line-clamp-2 mb-4 flex-grow leading-relaxed">
{post.description}
</p>
{/* Нижняя часть: Дата и Стрелочка */}
<div class="mt-auto pt-4 border-t border-neutral-100 dark:border-neutral-900 flex items-center justify-between">
<span class="text-xs font-medium text-neutral-400 dark:text-neutral-500">
{formattedDate}
</span>
{/* Иконка стрелочки (появляется при наведении) */}
<div class="transform translate-x-[-10px] opacity-0 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300 text-indigo-600 dark:text-indigo-400">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 8.25L21 12m0 0l-3.75 3.75M21 12H3" />
</svg>
</div>
</div>
</div>
{/*
3. ТЕГИ
Самый верхний слой (z-20), ВКЛЮЧАЕМ клики (pointer-events-auto)
Позиционируем их так, чтобы они не перекрывали текст, но были доступны
*/}
{post.tags && post.tags.length > 0 && (
<div class="absolute top-6 right-6 z-20 hidden sm:flex gap-2 pointer-events-auto">
{post.tags.slice(0, 2).map((tag) => (
<a
href={`/blog/tags/${tag.toLowerCase()}`}
class="text-[10px] uppercase font-bold tracking-wider text-neutral-400 hover:text-indigo-600 dark:hover:text-indigo-400 bg-neutral-50 dark:bg-neutral-900 px-2 py-1 rounded border border-neutral-100 dark:border-neutral-800 hover:border-indigo-200 transition-colors"
>
#{tag}
</a>
))}
</div>
)}
</div>
);
})
}

View file

@ -0,0 +1,163 @@
---
export interface Props {
id: number;
title: string;
description: string;
image: string;
category: string;
date: string;
readTime: string;
type: string;
featured?: boolean;
href?: string;
tags?: string[];
}
const {
id,
title,
description,
image,
category,
date,
readTime,
type,
featured = false,
href = "#",
tags = []
} = Astro.props;
---
{featured ? (
<!-- Featured Card (Large) - исправленная версия -->
<article class="group md:col-span-2 lg:col-span-2 cursor-pointer flex flex-col h-full bg-surface-dark rounded-lg border border-surface-border hover:border-primary transition-all duration-300 hover:-translate-y-1 overflow-hidden">
<!-- Изображение на всю ширину -->
<div class="relative aspect-[2/1] overflow-hidden">
<!-- Image -->
<img
class="w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-700"
alt={title}
src={image}
/>
<!-- Gradient overlay сверху изображения -->
<div class="absolute inset-0 bg-gradient-to-t from-surface-dark/95 via-surface-dark/30 to-transparent"></div>
<!-- Tags -->
{tags.length > 0 && (
<div class="absolute top-4 left-4 flex flex-wrap gap-2">
{tags.slice(0, 2).map(tag => (
<span class="bg-primary/90 text-background-dark text-xs font-bold px-2 py-1 rounded uppercase tracking-wider">
{tag}
</span>
))}
{tags.length > 2 && (
<span class="bg-surface-dark/90 text-white text-xs font-bold px-2 py-1 rounded uppercase tracking-wider">
+{tags.length - 2}
</span>
)}
</div>
)}
<!-- Content поверх изображения -->
<div class="absolute bottom-0 left-0 right-0 p-6 md:p-8">
<div class="flex flex-wrap items-center gap-3 mb-3">
<span class="bg-primary text-background-dark text-sm font-bold px-3 py-1.5 rounded uppercase tracking-wide">
{category}
</span>
<span class="text-gray-300 text-sm font-mono">{date}</span>
<span class="text-gray-400 text-sm flex items-center gap-1">
<span class="material-symbols-outlined text-[16px]">schedule</span>
{readTime}
</span>
</div>
<h3 class="text-white text-2xl md:text-3xl lg:text-4xl font-bold leading-tight mb-3 group-hover:text-primary transition-colors">
{title}
</h3>
<p class="text-gray-400 text-base md:text-lg line-clamp-2 max-w-2xl mb-4">
{description}
</p>
<a
href={href}
class="inline-flex items-center text-white text-sm font-bold uppercase tracking-wider hover:text-primary transition-colors group-hover:gap-2"
>
Читать полностью
<span class="material-symbols-outlined text-sm ml-1 group-hover:translate-x-1 transition-transform">arrow_forward</span>
</a>
</div>
</div>
</article>
) : (
<!-- Regular Card (остается без изменений) -->
<article class="group cursor-pointer flex flex-col h-full bg-surface-dark rounded-lg border border-surface-border hover:border-primary transition-all duration-300 hover:-translate-y-1 overflow-hidden">
<!-- Image -->
<div class="aspect-video overflow-hidden relative">
<img
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
alt={title}
src={image}
/>
<!-- Tags -->
{tags.length > 0 && (
<div class="absolute top-3 left-3 flex flex-wrap gap-2">
{tags.slice(0, 2).map(tag => (
<span class="bg-primary/90 text-background-dark text-xs font-bold px-2 py-1 rounded uppercase tracking-wider">
{tag}
</span>
))}
{tags.length > 2 && (
<span class="bg-surface-dark/90 text-white text-xs font-bold px-2 py-1 rounded uppercase tracking-wider">
+{tags.length - 2}
</span>
)}
</div>
)}
<!-- Type badge -->
<div class="absolute top-3 right-3 bg-background-dark/80 backdrop-blur px-3 py-1.5 rounded">
<span class="text-primary text-xs font-bold uppercase">{type}</span>
</div>
</div>
<!-- Content -->
<div class="p-5 md:p-6 flex flex-col flex-1">
<!-- Meta info -->
<div class="flex items-center gap-2 mb-3 text-xs text-gray-500 font-mono">
<span class="material-symbols-outlined text-[16px]">calendar_today</span>
<span>{date}</span>
<span class="mx-1">•</span>
<span class="flex items-center gap-1">
<span class="material-symbols-outlined text-[16px]">schedule</span>
{readTime}
</span>
</div>
<!-- Title -->
<h3 class="text-white text-lg md:text-xl font-bold leading-snug mb-3 group-hover:text-primary transition-colors line-clamp-2">
{title}
</h3>
<!-- Description -->
<p class="text-gray-400 text-sm md:text-base leading-relaxed mb-4 line-clamp-3 flex-1">
{description}
</p>
<!-- Category badge -->
<div class="mb-4">
<span class="inline-block bg-surface-border text-primary text-xs font-bold px-3 py-1.5 rounded uppercase tracking-wide">
{category}
</span>
</div>
<!-- Read more link -->
<a
href={href}
class="mt-auto pt-4 border-t border-surface-border flex items-center text-primary text-sm font-bold uppercase tracking-wider group-hover:gap-2 transition-all"
>
Читать далее
<span class="material-symbols-outlined text-sm ml-1 group-hover:translate-x-1 transition-transform">arrow_forward</span>
</a>
</div>
</article>
)}

View file

@ -0,0 +1,107 @@
---
import BlogCard from './BlogCard.astro';
const articles = [
{
id: 1,
title: "Перевозка 50-тонной газовой турбины: Логистический вызов",
description: "Подробный разбор сложнейшего маршрута через три климатические зоны. Как мы справились с обледенением дорог и нестандартными габаритами груза в рекордные сроки.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuASzPedaqnQMuVwGkd-98Tg1bVR6_fhPq8RWFQ88vPqNKVBr7OPg_HlBl1Fi6P6o4zvIO4T2JjTMNAHIajVYA3VYErT3JW4vTydFNQTYpwN0Yqf_RauKV7YQ2prA3o1cGzoCda3y5VOtztyvim-7I8wH6APkcL07uhgJzjlqY55L838i1ZFQnEHOLgTzZam6Qlzm_zIqIAuv6lB1WY5D0CUoMiH_MtA7TDzGyeEp_wU6l4YhrZXyj4fWe56NPzb7iHcIWjgzOWGkqI",
category: "Кейс месяца",
date: "12 Окт 2023",
readTime: "5 мин",
type: "Кейс",
featured: true,
href: "/blog/1",
tags: ["Негабарит", "Турбина", "Зимняя перевозка", "Сложный маршрут"]
},
{
id: 2,
title: "Изменения в таможенном законодательстве 2024",
description: "Новые пошлины и правила оформления грузов из Азии. Что нужно знать экспортерам, чтобы избежать задержек на границе в новом году.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuB8jD1asHPeNySZonpEMqohUKRO0RLCxOP-lTs8S1W1ZFtEywidG2hjWAJ5oe_RYjFFBkG7AxWPCjINCHTGYVDMUv_GrWKbJUzagJuWtIHUtIP0c_nv-vnW3gjtE6Vxm3UyRb6nA9C4LeBCmCUN8Ec7R9xlPxeaPhHtTIfRVLz20TQ9uRODtedHz7Vb5z9Fb4b9BoAW8TGw_vxRU55Pal4Wh2H55MJb-hgmULJ1orHCA_z7Lqi_u1qoL-5xhN3_CM4aPJiAg24i14Y",
category: "Новости",
date: "05 Ноя 2023",
readTime: "3 мин",
type: "Новости",
featured: false,
href: "/blog/2",
tags: ["Таможня", "Законодательство", "Экспорт", "Азия"]
},
{
id: 3,
title: "Особенности транспортировки химического оборудования",
description: "Специфика крепления и защиты чувствительных приборов при длительной перевозке. Рекомендации наших инженеров.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuCNs5R4PQbUV4fj0jg7SJve6-fXfE2uNAZ52CN5WhAPYQ4KxnZ7fAUeY-pAdCAgRZtYC5jq9OHsZEJ_94ZIRKjJaSpR_1QMppyCA2RD1L0Rpa--TPrPGxWTzw6Ur0YdAOhjWYyyH1HIDs8ilRWSOuHjuArYLomFdzI6fcIlg2f1atX-6lHSjmUqAfF4RjvtNYWis1y2gjJ1ezlf_6ncW76KnDSxzxl0XsGEbGcTxATdnJYVbvGwHIj0pOwy2vaVVjfXaMFAFqf2SsA",
category: "Советы",
date: "20 Сен 2023",
readTime: "5 мин",
type: "Советы",
featured: false,
href: "/blog/3",
tags: ["Химия", "Оборудование", "Защита груза", "Инженерные решения"]
},
{
id: 4,
title: "Логистика в условиях санкций: Новые маршруты",
description: "Анализ альтернативных транспортных коридоров \"Север-Юг\" и их эффективность для тяжелой промышленности в текущем квартале.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuBjGKAfZsRmz7XXguWuoZzL7Ivut1jGP50c_Ka4W7HWKSj2xRr5P0neZmy8Ce5sA9JFTkYnLgDzMTpa5YwKjlPBm3jZxhvzWMbmsCwZuXvPBEHndxzcInejjL7aRxfy1BjrEQg3srP7f0zkR0pbpMSnethVpM7qE8ySLBjszb4qzr0kLHRnA6UWmvaJ6k1moVGgiD407dzSm03RbFM8-aKeDagv-egBJ8Nc0APQrLVVC_rcZYtZqAhbuMEhYJz4FAeKjfUy6Mm92HY",
category: "Аналитика",
date: "15 Авг 2023",
readTime: "7 мин",
type: "Аналитика",
featured: false,
href: "/blog/4",
tags: ["Санкции", "Маршруты", "Аналитика", "Международная логистика"]
},
{
id: 5,
title: "Доставка буровой установки на Крайний Север",
description: "История о том, как мы доставляли оборудование весом 120 тонн в условиях полярной ночи и отсутствия дорог.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuA9pouQ9t8lmL5leE3iWTHuoO3dS_jPJth8bi4nMs9CZcLr-GpeegyxLXpv-snHICn7WW5RKIqgXRvUFlw5Nr7CtMPIUcc8abYOnyaxF0sx9PfgvqQHqS0rUCSR2QMQK3ubjvuCqZHHZAEJ6zboIxMN43ENLulI_RLfRBU5GeYdmWgU78izWUJgBu16BW1FMpfO7-a6OQRuGX7BiLRaBY1Vm02b9ERqDc4EdEsi6kjRhAXuHmThvrEzTPJrcE1G0JKzbFSKt41Tqqg",
category: "Кейсы",
date: "28 Июл 2023",
readTime: "4 мин",
type: "Кейсы",
featured: false,
href: "/blog/5",
tags: ["Буровая", "Крайний Север", "Сложные условия", "Проектная логистика"]
},
{
id: 6,
title: "Как выбрать подрядчика для проектных грузов",
description: "Чек-лист из 10 пунктов для проверки надежности логистического оператора. Не рискуйте дорогостоящим оборудованием.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuA5XX44tnNxtIO08iZQSE6ArxjugAesKJJXnG-c32AY3rGwCkz2PwU4f7QJzw6hgvUaQR2WkuLJu6vKXwvcKRJSjT_HKnsjir-Og1RJtohuIoE-_i-A32nqhtm90pF7lwfVQ-CfxoNYJXxsGcFoP8--bfDgBF-dS4-MfHhCgbiR79MolY7s38l34QCRzHLCRAnqcWaAqibzZTVo9jxCyTCdaNPpk4ZbuCIN6R9gnwL2xOE-7x6RZiK0wMu9o2jcpwUHKoJomWXAb60",
category: "Советы",
date: "10 Июл 2023",
readTime: "6 мин",
type: "Советы",
featured: false,
href: "/blog/6",
tags: ["Выбор подрядчика", "Чек-лист", "Советы", "Безопасность"]
}
];
---
<!-- Blog Grid -->
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full px-4 md:px-10 py-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{articles.map(article => (
<BlogCard
id={article.id}
title={article.title}
description={article.description}
image={article.image}
category={article.category}
date={article.date}
readTime={article.readTime}
type={article.type}
featured={article.featured}
href={article.href}
tags={article.tags}
/>
))}
</div>
</div>
</div>

View file

@ -0,0 +1,17 @@
<!-- Newsletter CTA -->
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full px-4 md:px-10 py-12">
<div class="w-full bg-primary/10 border border-primary/20 rounded-lg p-8 md:p-12 mt-4 flex flex-col md:flex-row items-center justify-between gap-8">
<div class="flex flex-col gap-2 max-w-lg">
<h3 class="text-white text-2xl font-bold uppercase">Подпишитесь на дайджест</h3>
<p class="text-gray-400">Получайте свежие новости логистики и разборы кейсов раз в неделю. Никакого спама.</p>
</div>
<div class="flex w-full md:w-auto gap-2">
<input class="bg-background-dark border border-surface-border text-white px-4 py-3 rounded w-full md:w-64 focus:border-primary focus:ring-1 focus:ring-primary outline-none" placeholder="Ваш Email" type="email"/>
<button class="bg-primary hover:bg-primary/90 text-background-dark font-bold px-6 py-3 rounded whitespace-nowrap">
Подписаться
</button>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,28 @@
---
import BlogCard from './BlogCard.astro';
const featuredArticle = {
id: 1,
title: "Перевозка 50-тонной газовой турбины: Логистический вызов",
description: "Подробный разбор сложнейшего маршрута через три климатические зоны. Как мы справились с обледенением дорог и нестандартными габаритами груза в рекордные сроки.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuASzPedaqnQMuVwGkd-98Tg1bVR6_fhPq8RWFQ88vPqNKVBr7OPg_HlBl1Fi6P6o4zvIO4T2JjTMNAHIajVYA3VYErT3JW4vTydFNQTYpwN0Yqf_RauKV7YQ2prA3o1cGzoCda3y5VOtztyvim-7I8wH6APkcL07uhgJzjlqY55L838i1ZFQnEHOLgTzZam6Qlzm_zIqIAuv6lB1WY5D0CUoMiH_MtA7TDzGyeEp_wU6l4YhrZXyj4fWe56NPzb7iHcIWjgzOWGkqI",
category: "Кейс месяца",
date: "12 Окт 2023",
readTime: "5 мин",
type: "Кейс",
href: "/blog/1",
tags: ["Негабарит", "Турбина", "Зимняя перевозка", "Сложный маршрут"]
};
---
<!-- Featured Article Section -->
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full">
<div class="mb-12">
<BlogCard
{...featuredArticle}
featured={true}
/>
</div>
</div>
</div>

View file

@ -0,0 +1,26 @@
---
const categories = [
{ name: "Все материалы", active: true },
{ name: "Новости отрасли", active: false },
{ name: "Кейсы", active: false },
{ name: "Советы экспертов", active: false },
{ name: "Проектная логистика", active: false }
];
---
<!-- Filters & Categories -->
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full px-4 md:px-10 py-6">
<div class="flex flex-wrap items-center gap-3">
{categories.map(category => (
<button
class={`${category.active
? 'px-5 py-2 rounded bg-primary text-background-dark font-bold text-sm transition-transform hover:-translate-y-0.5'
: 'px-5 py-2 rounded bg-surface-dark border border-surface-border text-gray-300 hover:text-white hover:border-primary/50 text-sm font-medium transition-all hover:-translate-y-0.5'}`}
>
{category.name}
</button>
))}
</div>
</div>
</div>

View file

@ -0,0 +1,11 @@
<!-- Pagination -->
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full px-4 md:px-10 py-8">
<div class="flex justify-center">
<button class="flex items-center gap-2 border border-surface-border hover:border-primary text-white bg-surface-dark hover:bg-surface-border px-8 py-3 rounded text-sm font-bold uppercase tracking-wider transition-all">
<span class="material-symbols-outlined">refresh</span>
Загрузить больше
</button>
</div>
</div>
</div>

View file

@ -0,0 +1,24 @@
---
---
<!-- CTA Section -->
<section class="relative overflow-hidden bg-primary py-16 sm:py-24">
<div class="absolute inset-0 opacity-10" style="background-image: repeating-linear-gradient(45deg, #000 0, #000 10px, transparent 10px, transparent 20px);">
</div>
<div class="relative mx-auto max-w-[1440px] px-4 text-center sm:px-6 lg:px-8">
<h2 class="mb-4 font-display text-4xl font-black uppercase leading-tight text-background-dark md:text-5xl">
Нужна техника под проект?
</h2>
<p class="mx-auto mb-10 max-w-2xl text-lg font-medium text-background-dark/80 font-body">
Мы подберем оптимальную конфигурацию автопоезда для вашего груза, рассчитаем нагрузку на оси и подготовим схему погрузки.
</p>
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row">
<button class="w-full rounded bg-background-dark px-8 py-4 text-lg font-bold text-white shadow-lg transition-transform hover:scale-105 active:scale-95 sm:w-auto">
Рассчитать проект
</button>
<button class="w-full rounded border-2 border-background-dark bg-transparent px-8 py-4 text-lg font-bold text-background-dark transition-colors hover:bg-background-dark/10 sm:w-auto">
Консультация инженера
</button>
</div>
</div>
</section>

View file

@ -0,0 +1,38 @@
---
---
<div class="relative w-full border-b border-border-dark">
<div class="absolute inset-0 bg-cover bg-center bg-no-repeat" data-alt="Heavy industrial truck carrying oversized cargo on a highway at dusk" style='background-image: linear-gradient(to right, rgba(24, 22, 17, 0.9) 0%, rgba(24, 22, 17, 0.6) 50%, rgba(24, 22, 17, 0.3) 100%), url("https://lh3.googleusercontent.com/aida-public/AB6AXuB6BcCz44s1O5618H0MG8SmWKZr4MQcgeLUPgAJWhgkyLwql9N-IUdqt1M83QtX3MxSAZ-wKrelQKjJlptWbmLjBh6nWXIBk5ipL2ROAAdOx8YWDvUFO8F7KC3LwRehTchZlnkcS3tQKBSDHZJpO9NOwkzK3mLwhCxGDJbPTfPTnX52o7dfgPLTPZ280b9Xu3W_tPoN-GK6exokTeHYxzEmDomr517HiBQKrn6CAlNwXWQXooOgpnG41PNXCmlGOw0Y1V9WXhsJ-Vc");'>
</div>
<div class="relative mx-auto max-w-[1440px] px-4 py-24 sm:px-6 lg:px-8 lg:py-32">
<div class="max-w-2xl">
<div class="inline-flex items-center gap-2 rounded border border-primary/30 bg-primary/10 px-3 py-1 mb-6 backdrop-blur-sm">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
</span>
<span class="text-xs font-bold uppercase tracking-widest text-primary">Собственный автопарк</span>
</div>
<h1 class="font-display text-5xl font-black uppercase leading-[0.9] tracking-tighter text-white sm:text-6xl md:text-7xl mb-6">
Мощность для<br/>
<span class="text-transparent bg-clip-text bg-gradient-to-r from-primary to-orange-400">Сложнейших</span> задач
</h1>
<p class="mb-8 max-w-lg text-lg text-white/70 font-body">
Специализированная техника мирового класса для решения нестандартных логистических задач. Надежность, подтвержденная годами эксплуатации в суровых условиях.
</p>
<div class="flex flex-col sm:flex-row gap-4">
<button class="flex h-12 items-center justify-center gap-2 rounded bg-primary px-8 text-base font-bold text-background-dark hover:bg-white transition-colors">
<span class="material-symbols-outlined text-[20px]">download</span>
Скачать каталог
</button>
<button class="flex h-12 items-center justify-center gap-2 rounded border border-white/20 bg-white/5 px-8 text-base font-bold text-white hover:bg-white/10 transition-colors backdrop-blur-sm">
<span class="material-symbols-outlined text-[20px]">play_circle</span>
Видео презентация
</button>
</div>
</div>
</div>
<!-- Tech decoration lines -->
<div class="absolute bottom-0 left-0 h-[1px] w-1/3 bg-gradient-to-r from-transparent via-primary to-transparent opacity-50"></div>
<div class="absolute bottom-0 right-0 h-12 w-[1px] bg-border-dark"></div>
</div>

View file

@ -0,0 +1,26 @@
---
---
<!-- Stats Section (Brutalist Bar) -->
<section class="w-full border-b border-border-dark bg-surface-dark py-8">
<div class="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-2 gap-8 md:grid-cols-4">
<div class="flex flex-col gap-1 border-l-2 border-primary pl-4">
<span class="text-3xl font-black text-white">50+</span>
<span class="text-xs uppercase tracking-wider text-text-muted">Единиц техники</span>
</div>
<div class="flex flex-col gap-1 border-l-2 border-border-dark pl-4 hover:border-primary transition-colors">
<span class="text-3xl font-black text-white">300т</span>
<span class="text-xs uppercase tracking-wider text-text-muted">Макс. грузоподъемность</span>
</div>
<div class="flex flex-col gap-1 border-l-2 border-border-dark pl-4 hover:border-primary transition-colors">
<span class="text-3xl font-black text-white">100%</span>
<span class="text-xs uppercase tracking-wider text-text-muted">Страхование грузов</span>
</div>
<div class="flex flex-col gap-1 border-l-2 border-border-dark pl-4 hover:border-primary transition-colors">
<span class="text-3xl font-black text-white">EURO 6</span>
<span class="text-xs uppercase tracking-wider text-text-muted">Экологический класс</span>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,155 @@
---
const trucks = [
{
id: 1,
category: "Тягачи",
categoryColor: "primary",
title: "Volvo FH16 750",
subtitle: "Седельный тягач тяжелого класса",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuAOaHRxL-iVeNx0_lb7j0y8TtfMpj0NNdiYeJJ2czCRUy8X_36_Jmd9Bc7Mm6BFI-gQqYb8LZjPSddEy4QYrn0hM5mJIdOmhjdI6irhh8XxIgoavYskuVcw8PCqFO8pQ4nkx2anLdRijpBU1uq8f69dpJulDJQjdHT-1GiFg804reWivl8J1N7f_efpITSRahIF4vhwmlYONcOM7ntb9AYP2ceiwOTA32lcd8YIpPmgI554tblSy1a9HuuPtxfOqnDsFLw0gd1TQfs",
specs: [
{ label: "Мощность", value: "750 л.с." },
{ label: "Кол. формула", value: "8x4 / 4" },
{ label: "Нагрузка ССУ", value: "35 тонн" },
{ label: "Полная масса", value: "до 325 т" }
]
},
{
id: 2,
category: "Тягачи",
categoryColor: "primary",
title: "Mercedes Actros SLT",
subtitle: "Балластный тягач",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuA9iwqEhWUwOzqAw9_gybpLjg00xOij9K_F_l9rzooWSj0bXPGSQJ4EzxoNYOG6IYlya3UIRzj6IqHM6flCU-VSlWk-IUqWKtBu1Aspbha9gqOIER1I29ezWrf3HsPpa5xUFB-nqD9P1LGK1c5LgPA-EwIU32KEzwgKnE8l-KKPmmXax1Ac32dToah6lP0wAyFnlvtXrmoGkouOfYfrHmPLTHg9hxJTrVbUaFTnhQ6h_Ka60KQh5L2zwL8KN1y5IqqCfi8djPPTcrQ",
specs: [
{ label: "Мощность", value: "625 л.с." },
{ label: "Кол. формула", value: "8x8" },
{ label: "Тяга", value: "Турбосцеп." },
{ label: "Автопоезд", value: "до 250 т" }
]
},
{
id: 3,
category: "Тралы",
categoryColor: "white",
title: "Goldhofer THP/SL",
subtitle: "Модульная система",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuB7zAoAh24UbH6tcFgydrKHdNM-bCAE7ud_IXTjAd_svLsrwYJCnBETygi4Nz6zVdtm1k32flW6BHjbWeLaodU8u0McTxcUIfQDK9HM35QZNV6WbDyR2t2qFwIP6evJHD1LpnT6H6A3WSJzoeiXtB8oiV-toD3WluHf1e4cAh1uCzDhUeNsVlB41Ti8GO8hdj_6ZTJ-nFf-Qmd23f4LgFZHGOf2IqkOzcEpXqHVo3omaeIHSiF9VCNIGhM5-Z1iYEZXCPyDUrjmh1U",
specs: [
{ label: "Нагрузка на ось", value: "45 т" },
{ label: "Конфигурация", value: "Модульная" },
{ label: "Высота", value: "1.175 мм" },
{ label: "Угол поворота", value: "+/- 55°" }
]
},
{
id: 4,
category: "Тралы",
categoryColor: "white",
title: "Faymonville TeleMAX",
subtitle: "Телескопический полуприцеп",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuBWhtkTXHNaeyJkrvzGBGd1zwVODkwvo5tjfxrBILzIUYLWoeLwBPLz7OuOmlfP76JW0LgW2FY9YomMU3jZqtUH8tJMZ6lwE3lBatgiyouiSXdSh_9Hk051HAD07_-Mpg09oGvAKD66GfXaDKrPSu4POO38cZg22sKf74iZzZtQdKgqrZV30iJitb3hBIxNUYNqsEI0fYFuMh2zcGW9jZn28cgiVM5mwBOidl8q3MDklKwGhKxDip0TX96ZDKFGFaqYE18TK4JKrBM",
specs: [
{ label: "Длина", value: "до 65 м" },
{ label: "Оси", value: "3 - 10" },
{ label: "Подвеска", value: "Пневмо" },
{ label: "Назначение", value: "Лопасти" }
]
},
{
id: 5,
category: "Краны",
categoryColor: "text-muted",
title: "Liebherr LTM 1120",
subtitle: "Мобильный кран",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuAZhlVFnMaIu7crqpY5UYDXD1mFuDN1Dd_6J-g1-o7V4h7h4PYgkaZf-yaD9jbFucgljTzW0VcSYYU0nkcYqsMFE-fdIHZhEenoxsexKdpEXhrzAOtxCcH3VJNpODMd6DqXtHRF6gi3b3drmU3OV4frVWS5c6O1557mEbHtpbM_A9pUjJtcESW9f2hWShjGjFC0m2YZRNBaJLo1L8CeherfHNZmiJleN7GAsqiAaRDUzuWrHR4fBfe-eJcQMOJKS97rE7UO5h1ZA7E",
specs: [
{ label: "Грузоподъем.", value: "120 т" },
{ label: "Стрела", value: "66 м" },
{ label: "Оси", value: "4" },
{ label: "Балласт", value: "31 т" }
]
},
{
id: 6,
category: "Сервис",
categoryColor: "text-muted",
title: "Пилотное Сопровождение",
subtitle: "Автомобили прикрытия",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuAPJZjIY94FPbdEq1HPChotRqKB6SP5OnaJgJ7_db7qbR6auYteNIkeXZKuXxY142d0HwqbjksUWcp-CwZ_qEDBHaDNrQNdmMjfjFK4DLhIANZKs2gLLN55L0T3st64Vbt3cuTZMyrZE7D-BDkXXEmsxmeLrPHoPSlJoMFr7YSSBqwKWOrXAPwqZqR1nWMz1n0KGygSYdnJHijrEdVQjvFBlyS_yy6s6gUEDoPFLGDT2JL1QUKff9lQ7h4aZqx8qnt_1ho7ODOjkVo",
specs: [
{ label: "Оборудование", value: "Спецсигналы" },
{ label: "Связь", value: "Радио УКВ" },
{ label: "Экипаж", value: "2 пилота" },
{ label: "Измерители", value: "Высотомеры" }
]
}
];
---
<div class="flex-1 py-12 lg:py-16">
<div class="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8">
<!-- Filter Header -->
<div class="flex flex-col gap-8 md:flex-row md:items-end md:justify-between mb-10">
<div>
<h2 class="text-3xl font-bold uppercase tracking-tight text-white mb-2">Категории Техники</h2>
<div class="h-1 w-20 bg-primary"></div>
</div>
<!-- Chips / Tabs -->
<div class="flex flex-wrap gap-2">
<button class="group flex h-10 items-center gap-2 rounded bg-primary px-4 text-sm font-bold text-background-dark ring-2 ring-primary ring-offset-2 ring-offset-background-dark">
<span class="material-symbols-outlined text-[20px]">grid_view</span>
Все
</button>
<button class="group flex h-10 items-center gap-2 rounded bg-surface-dark border border-border-dark px-4 text-sm font-medium text-white hover:border-primary hover:text-primary transition-all">
<span class="material-symbols-outlined text-[20px]">agriculture</span>
Тягачи
</button>
<button class="group flex h-10 items-center gap-2 rounded bg-surface-dark border border-border-dark px-4 text-sm font-medium text-white hover:border-primary hover:text-primary transition-all">
<span class="material-symbols-outlined text-[20px]">flatware</span>
Тралы
</button>
<button class="group flex h-10 items-center gap-2 rounded bg-surface-dark border border-border-dark px-4 text-sm font-medium text-white hover:border-primary hover:text-primary transition-all">
<span class="material-symbols-outlined text-[20px]">precision_manufacturing</span>
Краны
</button>
</div>
</div>
<!-- Grid -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{trucks.map(truck => (
<article class="group relative flex flex-col overflow-hidden rounded bg-surface-dark border border-border-dark hover:border-primary transition-all duration-300 shadow-xl hover:shadow-primary/10">
<div class="relative aspect-[4/3] w-full overflow-hidden bg-background-dark">
<div class={`absolute top-4 left-4 z-10 rounded px-2 py-1 text-xs font-bold uppercase text-background-dark ${truck.category === 'Тягачи' ? 'bg-primary' : truck.category === 'Тралы' ? 'bg-white' : 'bg-text-muted'}`}>
{truck.category}
</div>
<div class="h-full w-full bg-cover bg-center transition-transform duration-700 group-hover:scale-110" data-alt={truck.subtitle} style={`background-image: url("${truck.image}");`}></div>
<!-- Gradient overlay -->
<div class="absolute inset-0 bg-gradient-to-t from-surface-dark via-transparent to-transparent opacity-80"></div>
</div>
<div class="flex flex-1 flex-col p-6">
<div class="mb-4 flex items-start justify-between">
<div>
<h3 class="text-xl font-bold uppercase leading-tight text-white group-hover:text-primary transition-colors">{truck.title}</h3>
<p class="text-sm text-text-muted mt-1 font-body">{truck.subtitle}</p>
</div>
<span class="material-symbols-outlined text-border-dark group-hover:text-primary transition-colors">arrow_outward</span>
</div>
<!-- Specs Grid -->
<div class="mb-6 grid grid-cols-2 gap-y-4 gap-x-2 border-t border-border-dark/50 pt-4">
{truck.specs.map(spec => (
<div>
<span class="block text-[10px] uppercase tracking-wider text-text-muted">{spec.label}</span>
<span class="font-mono text-sm font-bold text-white">{spec.value}</span>
</div>
))}
</div>
<button class="mt-auto w-full rounded border border-border-dark bg-transparent py-2.5 text-sm font-bold uppercase text-white hover:bg-primary hover:text-background-dark hover:border-primary transition-all">
Характеристики
</button>
</div>
</article>
))}
</div>
</div>
</div>

View file

@ -0,0 +1,64 @@
---
import Button from '@components/base/Button.astro';
import type { FormRow, FormField } from '@globalInterfaces';
const formFields: FormRow[] = [
[
{ label: "Ваше имя", placeholder: "Иван Иванов", type: "text", name: "name" },
{ label: "Компания", placeholder: "Название организации", type: "text", name: "company" }
],
[
{ label: "Телефон", placeholder: "+7 (___) ___-__-__", type: "tel", name: "phone" },
{ label: "Email", placeholder: "mail@example.com", type: "email", name: "email" }
]
];
---
<div class="bg-white/5 border border-white/10 p-8 lg:p-12 rounded-lg sticky top-24">
<div class="mb-10">
<h3 class="text-2xl font-bold uppercase mb-2 font-display">Обратная связь</h3>
<p class="text-gray-500 dark:text-gray-400 font-body">Заполните форму, и наш менеджер свяжется с вами в течение 30 минут.</p>
</div>
<form class="flex flex-col gap-6">
{formFields.map((row: FormRow) => (
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{row.map((field: FormField) => (
<div class="flex flex-col gap-2">
<label class="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">{field.label}</label>
<input
type={field.type as any}
name={field.name}
placeholder={field.placeholder}
class="w-full bg-background-dark/50 border border-white/10 rounded px-4 py-3 text-white focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none transition-all placeholder-gray-600"
/>
</div>
))}
</div>
))}
<div class="flex flex-col gap-2">
<label class="text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">Сообщение / Детали груза</label>
<textarea
rows="5"
placeholder="Опишите задачу..."
class="w-full bg-background-dark/50 border border-white/10 rounded px-4 py-3 text-white focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none transition-all resize-none placeholder-gray-600"
></textarea>
</div>
<div class="flex items-start gap-3 mt-2">
<div class="relative flex items-start">
<input type="checkbox" id="privacy" class="peer h-5 w-5 cursor-pointer appearance-none rounded border border-white/20 bg-background-dark/50 checked:bg-primary checked:border-primary transition-all" />
<span class="absolute text-black opacity-0 peer-checked:opacity-100 top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none material-symbols-outlined text-sm font-bold">check</span>
</div>
<label for="privacy" class="text-sm text-gray-500 dark:text-gray-400 cursor-pointer select-none leading-tight font-body">
Я согласен с <a class="text-white hover:text-primary underline decoration-1 underline-offset-4" href="#">политикой конфиденциальности</a> и обработкой персональных данных
</label>
</div>
<!-- Используем универсальный Button -->
<Button variant="primary" type="submit" className="mt-4 w-full md:w-auto group">
Отправить запрос
<span class="material-symbols-outlined group-hover:translate-x-1 transition-transform">arrow_forward</span>
</Button>
</form>
</div>

View file

@ -0,0 +1,18 @@
<header class="relative py-6 lg:py-10 px-6 lg:px-12 border-b border-gray-200 dark:border-white/10 overflow-hidden">
<!-- Декоративный индустриальный элемент -->
<div class="absolute top-0 right-0 w-1/3 h-full bg-white/5 skew-x-12 translate-x-1/2 pointer-events-none"></div>
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full px-4 md:px-10 relative z-10">
<div class="flex flex-col gap-4">
<p class="text-primary font-bold uppercase tracking-[0.2em] text-sm">Мы всегда на связь</p>
<h2 class="text-5xl lg:text-7xl font-black uppercase tracking-tighter leading-[0.9] font-display">
Контакты
</h2>
<p class="text-gray-500 dark:text-gray-400 text-lg lg:text-xl max-w-2xl mt-2 font-light font-body">
Профессиональная логистика для ваших задач. Офис в Москве, работаем по всей России и СНГ.
</p>
</div>
</div>
</div>
</header>

View file

@ -0,0 +1,40 @@
---
import { SITE } from '@constants/site';
import type { ContactInfo } from '@globalInterfaces';
const contacts: readonly ContactInfo[] = SITE.CONTACTS.page;
---
<div class="flex flex-col">
<h3 class="text-2xl font-bold uppercase mb-8 border-l-4 border-primary pl-4 font-display">Информация</h3>
<div class="space-y-8">
{contacts.map((item) => (
<div class="group flex gap-5">
<div class="w-12 h-12 rounded bg-white/5 flex items-center justify-center shrink-0 border border-white/10 group-hover:border-primary/50 transition-colors">
<span class="material-symbols-outlined text-primary">{item.icon}</span>
</div>
<div>
<p class="text-xs uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">{item.label}</p>
{('isHtml' in item && item.isHtml) ? (
<p class="text-lg font-medium leading-tight" set:html={item.value} />
) : ('emails' in item && item.emails) ? (
item.emails.map((email: string) => (
<a href={`mailto:${email}`} class="text-lg font-medium leading-tight hover:text-primary transition-colors block mb-1">
{email}
</a>
))
) : (
<>
{('value' in item) && (
<a href={'link' in item ? item.link : '#'} class="text-lg font-medium leading-tight hover:text-primary transition-colors block">
{item.value}
</a>
)}
{'subValue' in item && item.subValue && <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{item.subValue}</p>}
</>
)}
</div>
</div>
))}
</div>
</div>

View file

@ -0,0 +1,23 @@
<div class="relative w-full aspect-video lg:aspect-square bg-gray-800 rounded overflow-hidden border border-white/10 group">
<img
alt="Карта проезда"
class="w-full h-full object-cover opacity-60 grayscale group-hover:grayscale-0 group-hover:opacity-40 transition-all duration-500"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuB4lozrbKdBEGhHB1pus7LmVc4bQFv_En-ejy1is_BbgBMuRGed1F0O_KCPGsAl_4X-CTRMA1suRGPdOFszGcHB_TaZUk8yg9mll28Kqc_h-wAB1Q3Zv0tK8YkL9vM5Hn6AwnIPFz8AgaYQtmYQHCMUiU7i7dQKDOCtzENdQhevaXBfnMR8_HNkG_ydb30ygTvNGgrBUtqIJO2FQy17FG3HS3MmWREQjEPr1nXp477W5ht6CcT-oeIi_wocyWs3OBowgNeS_GncjBc"
/>
<!-- Анимированный пин -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center">
<div class="w-4 h-4 bg-primary rounded-full animate-ping absolute"></div>
<div class="w-10 h-10 bg-primary text-black rounded-full flex items-center justify-center shadow-lg shadow-primary/20 relative z-10">
<span class="material-symbols-outlined text-xl">location_on</span>
</div>
<div class="bg-background-dark/90 backdrop-blur border border-white/10 px-3 py-1 mt-2 rounded text-xs font-bold uppercase tracking-wider">
Офис
</div>
</div>
<a class="absolute bottom-4 right-4 bg-white/10 hover:bg-primary hover:text-black backdrop-blur-md px-4 py-2 rounded text-xs font-bold uppercase tracking-wide border border-white/10 transition-colors flex items-center gap-2" href="#">
Открыть карту
<span class="material-symbols-outlined text-sm">open_in_new</span>
</a>
</div>

View file

@ -0,0 +1,39 @@
---
import Button from '@components/base/Button.astro';
---
<div class="w-full bg-primary relative overflow-hidden" id="calculate">
<!-- Industrial stripes pattern -->
<div class="absolute inset-0 opacity-10" style="background-image: repeating-linear-gradient(45deg, #000 0, #000 10px, transparent 10px, transparent 20px);"></div>
<div class="layout-container flex justify-center w-full relative z-10">
<div class="layout-content-container flex flex-col max-w-[1280px] w-full px-4 md:px-10 py-20">
<div class="flex flex-col lg:flex-row items-center justify-between gap-10">
<div class="flex flex-col gap-4 max-w-[700px]">
<h2 class="text-background-dark text-4xl md:text-5xl font-black font-display uppercase leading-none tracking-tight">
Готовы начать проект?
</h2>
<p class="text-background-dark/80 text-lg font-medium max-w-[600px] border-l-4 border-background-dark pl-4">
Оставьте заявку на расчет стоимости перевозки вашего груза прямо сейчас. Наши инженеры подготовят коммерческое предложение в течение 2 часов.
</p>
</div>
<div class="flex flex-col sm:flex-row gap-4 w-full lg:w-auto">
<!-- Темная кнопка -->
<Button variant="dark">
Оставить заявку
</Button>
<!-- Кнопка с иконкой телефона (используем кастомные классы для border) -->
<Button
href="tel:88005553535"
variant="dark"
className="bg-transparent border-2 border-background-dark text-background-dark hover:bg-background-dark/10 hover:shadow-none"
>
<span class="material-symbols-outlined mr-2">call</span> 8 (800) 555-35-35
</Button>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,68 @@
---
import heroImage from '@assets/images/hero/heroImg.avif';
import Button from '@components/base/Button.astro';
const truckList = [
{ model: "MAN TGX 6x4", width: "w-8" },
{ model: "VOLVO FH16", width: "w-4" },
{ model: "SCANIA R730", width: "w-4" },
];
---
<div class="relative w-full">
<div class="relative flex min-h-[90vh] md:min-h-[85vh] flex-col items-center justify-center overflow-hidden bg-cover bg-center bg-no-repeat"
style={`background-image: linear-gradient(rgba(17, 24, 39, 0.7) 0%, rgba(17, 24, 39, 0.4) 50%, rgba(26, 30, 35, 1) 100%), url("${heroImage.src}");`}>
<div class="layout-content-container flex flex-col max-w-[1280px] w-full px-4 md:px-10 z-10">
<!-- Группирующий контейнер с адаптивным выравниванием -->
<div class="flex flex-col items-center md:items-start text-center md:text-left gap-6 max-w-[900px] mx-auto md:mx-0">
<!-- Верхний бейдж -->
<div class="inline-flex items-center gap-2 px-3 py-1 rounded bg-primary/20 border border-primary/30 w-fit backdrop-blur-md">
<span class="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
<span class="text-primary text-[10px] md:text-xs font-bold uppercase tracking-[0.2em] font-display">Логистика проектных грузов</span>
</div>
<!-- ЗАГОЛОВОК -->
<h1 class="text-white text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-black font-display leading-[1.1] md:leading-[1.05] tracking-tight uppercase drop-shadow-2xl break-words w-full">
Негабаритные<br class="hidden sm:block"/>
<span class="text-transparent bg-clip-text bg-gradient-to-r from-white via-white to-gray-500">перевозки</span><br/>
<span class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl opacity-90">любой сложности</span>
</h1>
<!-- Описание -->
<!-- border-l-0 на мобилках, md:border-l-4 на десктопе -->
<p class="text-gray-300 text-base md:text-lg lg:text-xl font-light max-w-[600px] border-l-0 md:border-l-4 border-primary pl-0 md:pl-6 leading-relaxed">
Перевозка промышленного оборудования и спецтехники по всей России.
<br class="hidden md:block"/>
<span class="text-white font-medium italic">Надежность. Точность. Масштаб.</span>
</p>
<!-- Кнопки -->
<div class="flex flex-col sm:flex-row gap-4 mt-6 md:mt-4 w-full sm:w-auto items-center">
<Button href="#calculate" variant="primary" className="w-full sm:w-auto text-sm md:text-base min-w-[200px]">
Рассчитать стоимость
</Button>
<Button href="#fleet" variant="outline" className="w-full sm:w-auto text-sm md:text-base min-w-[200px]">
Наш автопарк
</Button>
</div>
</div>
</div>
<!-- Декоративные элементы -->
<div class="absolute bottom-0 left-0 w-full h-32 bg-gradient-to-t from-[#181611] to-transparent"></div>
<!-- Список техники (по-прежнему только на больших экранах) -->
<div class="absolute right-10 bottom-10 hidden xl:flex flex-col gap-4 text-white/30 font-display text-[10px] uppercase tracking-[0.3em]">
{truckList.map((truck, index) => (
<div class="flex items-center gap-3 hover:text-primary transition-colors cursor-default group">
<span class={`h-[1px] bg-primary group-hover:w-12 transition-all ${truck.width}`}></span>
0{index + 1} {truck.model}
</div>
))}
</div>
</div>
</div>

View file

@ -0,0 +1,48 @@
---
const projects = [
{
title: "Перевозка газовой турбины",
location: "Новый Уренгой — 120 тонн",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuBFnYR6nvSIaun98fKMCUzNf2NAbXrvHR0iT1HTCU-V3KzLwyIoVngr3loAMRyk26Is-peou-H2856C6X28njF1iqa3WkcCkbE5zjyperulaDQPDhOVSpJJscW1QO8keTWUKvw0vkr1Rf4sgtXugoC3JqhcpWPZbEwftW43fUJGCK-HWoJNc3CCnLwvhKBTssFvUpsI2VWp7nWL0Uk9Pu7gr3GVs_d7HfIYCLj5dFiGpHPzJn0hwqI8jYpvaHPjh1xI44Wjw6dnwQ4",
span: true // Флаг для карточки на 2 колонки
},
{
title: "Экскаватор Komatsu",
location: "Кемерово",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuBM3jebRsft87-UGor5cDj-5XeQgHbD3Je6WA551uxU-eHh2vNW5F9adLRJ_0Y9-Cn09mLIXcGi8L7xHvlIENoRD9K0zV7V_7cgTevo8IKg2yX_91A1lVterPwFRTtx6zkrwhFJKblvsYeObMJb-y3eCKy1LdeBrBdhQJLduDDk1_Hm6YOPjQrUqfGhApLYXlitOuGfR_RWNUlCxKQLSppWTJ04QUDBefgbJqocaTeCvjspsXVVRepuUl2CaYnpVg-r9arNzxYSsbY",
span: false
},
{
title: "Трубы большого диаметра",
location: "Омск",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuAFXY9zlCrElJmxX97AMfD9B4XIumIYOAIQ2izDbagOpw7p5rEK9JhWdRNzhzeyEBjbMu3BKdXPlOR_AUv7Iz-QjdkaSYeDIl9GTbian3cKZ4-MNzLMHpq3eHWSWVNIZWEP8I_VnAg-1trD91_PqlKVgX4ggj6IHYztEjp2dIwbPmIYSG9kfWmTbPhcm6bRvjhBNTES9qpxao1PAs_ytyiiRqPw91E1VdYXhipdKhHnOAg_VMRGUU7IKNjXixCYjQ7GSEEMzQixlSM",
span: false
}
];
---
<div class="w-full bg-[#15181c] py-20 border-t border-white/5" id="projects">
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1280px] w-full px-4 md:px-10">
<div class="flex flex-col gap-2 mb-10">
<span class="text-primary font-bold font-display uppercase tracking-widest text-sm">Наш опыт</span>
<h2 class="text-white text-3xl md:text-4xl font-black font-display uppercase">Реализованные проекты</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 auto-rows-[300px]">
{projects.map((project) => (
// Условный класс для col-span
<div class={`group relative overflow-hidden rounded bg-gray-800 row-span-1 ${project.span ? 'lg:col-span-2' : ''}`}>
<div class="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110 grayscale group-hover:grayscale-0"
style={`background-image: url("${project.image}");`}></div>
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent opacity-80 group-hover:opacity-60 transition-opacity"></div>
<div class="absolute bottom-0 left-0 p-6">
<h3 class="text-white text-xl font-bold font-display uppercase mb-1">{project.title}</h3>
<p class="text-primary text-sm font-medium">{project.location}</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,62 @@
---
const services = [
{
icon: "scale",
bgIcon: "balance",
title: "Тяжеловесные грузы",
desc: "Собственный парк специализированных модульных тралов для перевозки грузов весом до 500 тонн. Инженерный расчет нагрузок."
},
{
icon: "straighten",
bgIcon: "straighten",
title: "Негабаритные размеры",
desc: "Разработка индивидуальных маршрутов для грузов, превышающих стандартные габариты. Сопровождение машинами прикрытия и ДПС."
},
{
icon: "description",
bgIcon: "description",
title: "Документация",
desc: "Полное юридическое сопровождение. Оформление спецразрешений (КТГ) в Росавтодоре. Страхование грузов 'от всех рисков'."
}
];
---
<div class="w-full bg-background-dark py-20 relative overflow-hidden">
<div class="absolute top-0 right-0 w-1/3 h-full bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] opacity-5 pointer-events-none"></div>
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1280px] w-full px-4 md:px-10 z-10">
<div class="flex flex-col md:flex-row justify-between items-end mb-16 gap-6">
<div class="flex flex-col gap-4">
<h2 class="text-white text-4xl md:text-5xl font-black font-display uppercase leading-none tracking-tight">
Технологичность<br/><span class="text-primary">и Точность</span>
</h2>
<p class="text-text-secondary text-base max-w-[500px]">
Обеспечиваем полный цикл логистики для проектных грузов, от инженерного планирования маршрута до выгрузки на объекте.
</p>
</div>
<a href="#" class="text-white hover:text-primary flex items-center gap-2 font-bold uppercase text-sm tracking-widest border-b border-white/20 pb-1 hover:border-primary transition-colors">
Все услуги компании <span class="material-symbols-outlined text-lg">arrow_forward</span>
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{services.map(service => (
<div class="group relative flex flex-col gap-6 p-8 rounded bg-surface-dark border border-white/5 hover:border-primary/50 transition-all duration-300 hover:-translate-y-1">
<div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<span class="material-symbols-outlined text-9xl">{service.bgIcon}</span>
</div>
<div class="size-16 flex items-center justify-center rounded bg-background-dark border border-white/10 group-hover:border-primary/50 group-hover:shadow-[0_0_15px_rgba(242,166,13,0.2)] transition-all">
<span class="material-symbols-outlined text-primary text-4xl">{service.icon}</span>
</div>
<div class="flex flex-col gap-2 z-10">
<h3 class="text-white text-xl font-bold font-display uppercase">{service.title}</h3>
<div class="h-[2px] w-12 bg-primary my-2"></div>
<p class="text-text-secondary text-sm leading-relaxed">{service.desc}</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,83 @@
---
import { SITE } from '@constants/site';
const currentYear = new Date().getFullYear();
const footerLinks = {
company: [
{ text: "Блог", href: "/blog" },
{ text: "Новости", href: "/news" },
{ text: "Вакансии", href: "#" },
{ text: "Инструкции", href: "/instructions" },
],
services: [
{ text: "Перевозка спецтехники", href: "#" },
{ text: "Промышленные переезды", href: "#" },
{ text: "Аренда тралов", href: "#" },
{ text: "Сопровождение грузов", href: "#" }
]
};
---
<footer class="w-full bg-[#111316] border-t border-[#393328] pt-16 pb-8" id="contacts">
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1280px] w-full px-4 md:px-10">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10 mb-16">
<!-- Branding -->
<div class="flex flex-col gap-6">
<div class="flex items-center gap-3 text-white">
<div class="flex items-center justify-center size-8 bg-primary/20 rounded">
<span class="material-symbols-outlined text-primary text-xl">local_shipping</span>
</div>
<h2 class="text-white text-lg font-bold font-display uppercase tracking-tight">KHIMTRANS</h2>
</div>
<p class="text-text-secondary text-sm leading-relaxed">
{SITE.TAGLINE}
</p>
<div class="flex gap-4">
{['public', 'send', 'chat'].map(icon => (
<a href="#" class="text-text-secondary hover:text-primary transition-colors">
<span class="material-symbols-outlined">{icon}</span>
</a>
))}
</div>
</div>
<!-- Links Columns (Map) -->
<div class="flex flex-col gap-4">
<h3 class="text-white text-base font-bold font-display uppercase tracking-widest text-primary">Компания</h3>
{footerLinks.company.map(link => (
<a href={link.href} class="text-text-secondary hover:text-white text-sm transition-colors">{link.text}</a>
))}
</div>
<div class="flex flex-col gap-4">
<h3 class="text-white text-base font-bold font-display uppercase tracking-widest text-primary">Услуги</h3>
{footerLinks.services.map(link => (
<a href={link.href} class="text-text-secondary hover:text-white text-sm transition-colors">{link.text}</a>
))}
</div>
<!-- Contacts -->
<div class="flex flex-col gap-4">
<h3 class="text-white text-base font-bold font-display uppercase tracking-widest text-primary">Контакты</h3>
{SITE.CONTACTS.footer.map(contact => (
<div class="flex items-center gap-3 text-text-secondary text-sm">
<span class="material-symbols-outlined text-primary text-lg">{contact.icon}</span>
<span>{contact.text}</span>
</div>
))}
</div>
</div>
<div class="w-full h-[1px] bg-white/10 mb-8"></div>
<div class="flex flex-col md:flex-row items-center justify-between gap-4 text-xs text-text-secondary">
<p>© {currentYear} {SITE.TITLE}. Все права защищены.</p>
<div class="flex gap-6">
<a href="#" class="hover:text-white transition-colors">Политика конфиденциальности</a>
<a href="#" class="hover:text-white transition-colors">Условия использования</a>
</div>
</div>
</div>
</div>
</footer>

View file

@ -0,0 +1,131 @@
---
import Button from '@components/base/Button.astro';
import MobileMenu from './MobileMenu.astro';
const currentPath = Astro.url.pathname;
const cleanPath = currentPath.replace(/\/$/, "") || "/";
const baseLinks = [
{ text: "Услуги", href: "/services" },
{ text: "Автопарк", href: "/cars" },
{ text: "Проекты", href: "/projects" },
{ text: "Контакты", href: "/contacts" },
];
const navLinks = cleanPath !== "/" ? [{ text: "Главная", href: "/" }, ...baseLinks] : baseLinks;
---
<div id="main-header" class="sticky top-0 z-[100] w-full border-b border-[#393328] bg-[#181611]">
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full px-4 md:px-10">
<header class="flex items-center justify-between py-4 relative">
<!-- Логотип основного хедера -->
<a href="/" class="flex items-center gap-4 text-white group relative z-10">
<div class="size-10 bg-primary/20 rounded flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-2xl">local_shipping</span>
</div>
<div class="flex flex-col">
<h2 class="text-white text-xl font-bold font-display leading-none tracking-tight uppercase">HIMTRANS</h2>
<span class="text-[10px] text-text-secondary font-display tracking-widest uppercase font-semibold">Service</span>
</div>
</a>
<!-- Desktop Nav -->
<div class="hidden lg:flex flex-1 justify-end gap-8 items-center">
<nav class="flex items-center gap-8">
{navLinks.map((link) => (
<a href={link.href} class="text-white/80 hover:text-primary text-sm font-medium transition-colors font-display uppercase tracking-wide">
{link.text}
</a>
))}
</nav>
<Button href="#contacts" variant="outline" className="h-9 px-5 text-xs border-white/20">
Связаться
</Button>
</div>
<!-- КНОПКА ГАМБУРГЕР -->
<button
id="menu-toggle"
class="lg:hidden flex flex-col gap-[8px] p-2 relative z-[160] focus:outline-none"
aria-label="Menu"
>
<div class="hamburger-line line-1"></div>
<div class="hamburger-line line-2"></div>
<div class="hamburger-line line-3"></div>
</button>
</header>
</div>
</div>
<!-- АНИМАЦИОННАЯ ЛИНИЯ ПРОГРЕССА -->
<div
id="scroll-line"
class="absolute bottom-0 left-0 h-[2px] bg-primary z-20 will-change-[width] shadow-[0_0_10px_rgba(242,166,13,0.7)]"
style="width: 0%"
></div>
<!-- Мобильное меню -->
<MobileMenu links={navLinks} />
</div>
<script>
const menuToggle = document.getElementById('menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
const overlay = document.getElementById('menu-overlay');
const scrollLine = document.getElementById('scroll-line');
const mobileLinks = document.querySelectorAll('.mobile-link');
// --- ЛОГИКА АНИМАЦИОННОЙ ЛИНИИ ---
function updateScrollProgress() {
if (!scrollLine) return;
// Сколько прокручено
const winScroll = window.scrollY || document.documentElement.scrollTop;
// Общая доступная высота (высота всего документа - высота видимого окна)
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
// Рассчитываем процент
const scrolled = height > 0 ? (winScroll / height) * 100 : 0;
// Обновляем ширину
scrollLine.style.width = scrolled + "%";
}
// --- ЛОГИКА МЕНЮ ---
function toggleMenu() {
const isActive = menuToggle?.classList.contains('is-active');
if (isActive) {
menuToggle?.classList.remove('is-active');
mobileMenu?.classList.add('-translate-x-full');
overlay?.classList.add('opacity-0', 'pointer-events-none');
document.body.style.overflow = '';
} else {
menuToggle?.classList.add('is-active');
mobileMenu?.classList.remove('-translate-x-full');
overlay?.classList.remove('opacity-0', 'pointer-events-none');
document.body.style.overflow = 'hidden';
}
}
// Слушатели событий
menuToggle?.addEventListener('click', toggleMenu);
overlay?.addEventListener('click', toggleMenu);
mobileLinks.forEach(l => l.addEventListener('click', toggleMenu));
// Оптимизированный слушатель скролла
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
updateScrollProgress();
ticking = false;
});
ticking = true;
}
});
// Инициализация при загрузке и ресайзе
window.addEventListener('DOMContentLoaded', updateScrollProgress);
window.addEventListener('resize', updateScrollProgress);
</script>

View file

@ -0,0 +1,61 @@
---
// src/components/layout/header/MobileMenu.astro
import Button from '@components/base/Button.astro';
interface Props {
links: { text: string; href: string }[];
}
const { links } = Astro.props;
---
<!-- 1. ОВЕРЛЕЙ -->
<div
id="menu-overlay"
class="fixed inset-0 bg-black/80 z-[140] opacity-0 pointer-events-none transition-opacity duration-500 lg:hidden"
></div>
<!-- 2. DRAWER (Боковая панель) -->
<div
id="mobile-menu"
class="fixed top-0 left-0 bottom-0 w-[85%] max-w-[400px] bg-[#181611] z-[150] -translate-x-full transition-transform duration-500 ease-in-out lg:hidden flex flex-col shadow-[20px_0_60px_rgba(0,0,0,0.5)] border-r border-white/5"
>
<!-- ШАПКА ВНУТРИ МЕНЮ (Теперь здесь лого вместо текста "Навигация") -->
<div class="p-6 h-20 flex items-center border-b border-white/5 bg-[#181611]">
<div class="flex items-center gap-4 text-white">
<div class="size-10 bg-primary/20 rounded flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-2xl">local_shipping</span>
</div>
<div class="flex flex-col">
<h2 class="text-white text-xl font-bold font-display leading-none tracking-tight uppercase">KHIMTRANS</h2>
<span class="text-[10px] text-text-secondary font-display tracking-widest uppercase font-semibold">Service</span>
</div>
</div>
</div>
<!-- СПИСОК ССЫЛОК -->
<div class="flex-grow overflow-y-auto p-8 bg-[#181611]">
<nav class="flex flex-col gap-8">
{links.map((link, index) => (
<a
href={link.href}
class="text-3xl font-display font-black uppercase tracking-tighter text-white hover:text-primary transition-colors flex items-baseline gap-4 group mobile-link"
>
<span class="text-xs font-mono text-gray-700">0{index + 1}</span>
{link.text}
</a>
))}
</nav>
</div>
<!-- ФУТЕР МЕНЮ -->
<div class="p-8 bg-[#13110d] border-t border-white/5">
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-1">
<span class="text-[10px] text-gray-500 uppercase tracking-[0.2em]">Круглосуточно:</span>
<a href="tel:88005553535" class="text-xl font-bold text-white hover:text-primary transition-colors">
8 (800) 555-35-35
</a>
</div>
<Button href="/contacts" variant="primary" className="w-full justify-center">Оставить заявку</Button>
</div>
</div>
</div>

View file

@ -0,0 +1,43 @@
---
const featuredNews = {
title: "Успешная доставка негабаритного реактора на Омский НПЗ",
description: "Завершился один из самых сложных логистических проектов года. Транспортировка реактора весом 350 тонн потребовала использования специализированных модульных платформ и укрепления дорожного полотна на маршруте в 1200 км.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuC0EvD1oFw0HZxXNNa1-ZL_XmBpWduHjqCoPZl8WVO3-RDNVCyQSQSJFfvYWTepp_RUfXneYxgdU0DUjznZ-dW3QRXKbs1WWwA1krAERvxvWS-uaqRhVnwLBILm3JyYvNZROGE2OjqS7cn3LW6VsRnmSOQpcL9xFnh8X-Q9yYH4ZZ0TFoTGXvYKtKVG_FSFVi9IoGhLX3aFZMPqeYBP274LUHFgHQucvymzSf5O1iS6xNA-OzgeiGLGfLXKBeGH1fWLGXoisVG1uws",
category: "Важное",
date: "12 Октября 2023"
};
---
<!-- Featured News Section - Полноэкранный вариант -->
<div class="w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full px-4 md:px-10 pb-12">
<div class="@container">
<div class="flex flex-col gap-6 py-6 lg:py-10 @[864px]:flex-row border border-border-dark bg-surface-dark rounded-lg overflow-hidden group hover:border-primary/50 transition-colors">
<div class="w-full @[864px]:w-1/2 h-64 @[864px]:h-auto relative overflow-hidden">
<div class="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-105" data-alt={featuredNews.title} style={`background-image: url("${featuredNews.image}");`}>
</div>
<div class="absolute top-4 left-4 bg-primary text-background-dark text-xs font-bold px-3 py-1 rounded uppercase tracking-wider">
{featuredNews.category}
</div>
</div>
<div class="flex flex-col justify-center gap-6 p-6 @[864px]:w-1/2 @[864px]:pl-10 @[864px]:pr-20">
<div class="flex flex-col gap-3 text-left">
<span class="text-primary font-mono text-sm">{featuredNews.date}</span>
<h1 class="text-white text-3xl lg:text-4xl font-black leading-tight tracking-[-0.033em]">
{featuredNews.title}
</h1>
<p class="text-text-secondary text-base font-normal leading-relaxed mt-2 line-clamp-3">
{featuredNews.description}
</p>
</div>
<div class="flex items-center gap-4 mt-2">
<button class="flex items-center justify-center overflow-hidden rounded h-12 px-6 bg-primary hover:bg-yellow-500 text-background-dark text-base font-bold leading-normal tracking-[0.015em] transition-colors">
Читать полностью
</button>
<div class="h-[1px] flex-1 bg-border-dark"></div>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,38 @@
---
const categories = [
{ name: "Все новости", active: true },
{ name: "Проекты", active: false },
{ name: "Корпоративные", active: false },
{ name: "События", active: false }
];
---
<!-- Filters & Search Toolbar -->
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full px-4 md:px-10">
<div class="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-6 mb-8 border-b border-border-dark pb-6">
<div class="flex gap-2 flex-wrap">
{categories.map(category => (
<button
class={`${category.active
? 'flex h-9 items-center justify-center gap-x-2 rounded bg-primary px-4 transition-colors'
: 'flex h-9 items-center justify-center gap-x-2 rounded bg-surface-dark border border-border-dark hover:border-text-secondary px-4 transition-colors'}`}
>
<span class={`${category.active
? 'text-background-dark text-sm font-bold leading-normal'
: 'text-text-secondary hover:text-white text-sm font-medium leading-normal'}`}>
{category.name}
</span>
</button>
))}
</div>
<div class="flex items-center w-full lg:w-auto gap-4">
<!-- Фильтр по дате -->
<div class="flex items-center gap-2 text-text-secondary text-sm font-medium">
<span class="material-symbols-outlined text-[18px]">filter_list</span>
<span>Фильтр по категории</span>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,78 @@
---
export interface Props {
id: number;
title: string;
description: string;
image: string;
date: string;
tags?: string[];
href?: string;
}
const {
id,
title,
description,
image,
date,
tags = [],
href = "#"
} = Astro.props;
---
<article class="group flex flex-col bg-surface-dark rounded-lg overflow-hidden border border-border-dark hover:border-primary transition-colors duration-300 h-full">
<!-- Изображение -->
<div class="relative h-56 overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center transition-transform duration-500 group-hover:scale-105"
data-alt={title}
style={`background-image: url("${image}");`}
>
</div>
<div class="absolute bottom-0 left-0 w-full h-1/2 bg-gradient-to-t from-surface-dark to-transparent"></div>
<!-- Теги (если есть) -->
{tags.length > 0 && (
<div class="absolute top-3 left-3 flex flex-wrap gap-2">
{tags.slice(0, 2).map(tag => (
<span class="bg-primary/90 text-background-dark text-xs font-bold px-2 py-1 rounded uppercase tracking-wider">
{tag}
</span>
))}
{tags.length > 2 && (
<span class="bg-surface-dark/90 text-white text-xs font-bold px-2 py-1 rounded uppercase tracking-wider">
+{tags.length - 2}
</span>
)}
</div>
)}
</div>
<!-- Контент -->
<div class="flex flex-col flex-1 p-6">
<!-- Дата -->
<div class="flex items-center gap-2 mb-3">
<span class="material-symbols-outlined text-primary text-[16px]">calendar_today</span>
<span class="text-primary text-xs font-bold uppercase tracking-wider">{date}</span>
</div>
<!-- Заголовок -->
<h3 class="text-white text-xl font-bold leading-tight mb-3 group-hover:text-primary transition-colors line-clamp-2">
{title}
</h3>
<!-- Описание -->
<p class="text-text-secondary text-sm leading-relaxed line-clamp-3 mb-4 flex-1">
{description}
</p>
<!-- Ссылка -->
<a
class="inline-flex items-center text-white text-sm font-bold uppercase tracking-wide hover:text-primary transition-colors gap-2 mt-auto"
href={href}
>
Подробнее
<span class="material-symbols-outlined text-[16px]">arrow_forward</span>
</a>
</div>
</article>

View file

@ -0,0 +1,79 @@
---
import NewsCard from './NewsCard.astro';
const news = [
{
id: 1,
title: "Расширение автопарка: новые тягачи для сверхтяжелых грузов",
description: "Компания пополнила свой парк десятью новыми тягачами Volvo 8x4, специально модифицированными для работы в условиях Крайнего Севера.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuDdz9UltPzp-PVnc1bnoYD4mKGJStD7bqLEFdHQ5G5SBUUUPV3dQ1RfnnHmGu63nLpD6sH4Zo9iu512XAUbP3M_694mzkKjGw9Sf6ukjkykjIq4wv_7E47PEnWlLFUx-XvvM8bSqirCb53H_nQnsvGRFYZ5uas-5Di0eRgagT-Jh9sW3VnHzV3bPrRc02MLdKrCCeqqRBLTW4EPR6hUE2Dy2xX3AL9YhT-MXUDTPATWhqclJqe56vw10BXnMdZONL2yHuvv_jYSjrk",
date: "28 Сентября 2023",
tags: ["Техника", "Инвестиции"],
href: "/news/1"
},
{
id: 2,
title: "Финансовый отчет за III квартал: рост международных перевозок",
description: "Объем экспортных операций вырос на 20% по сравнению с аналогичным периодом прошлого года, несмотря на усложнение логистических цепочек.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuBWQwpqVb4GQglISFsyFsPmwA6hEwedjDhup3DQdnIe2UeCunEQO_7WitLIIzRpSUbpSHyqMtbOx0zT274H_59VpvgVs-R6J4_pSnlet5gnEiXFmdgaUmbRdchQp0dMUosAeytGWUlkjB-M5HQht_dhJClmQ-jZ5UCdu3UbMlqYyIWCN-MxqkZJLS17Gu_yRIhOD1yr-PgbrP6xfmgR4u9zwHPnBbDTUDHuHL2uqI5cO9KzAXGYXLTM1FIlJlVYUFXq3TozA6_a2ho",
date: "15 Августа 2023",
tags: ["Финансы", "Экспорт"],
href: "/news/2"
},
{
id: 3,
title: "ХимТрансСервис на выставке TransRussia 2024",
description: "Приглашаем партнеров и клиентов посетить наш стенд. Мы представим новые решения для мультимодальных перевозок негабаритных грузов.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuAmv7fb2c0YscPrCcTb9AwbFbJQTwA2tWdFqKCGqTIByVt8WxeyKtg1NMU8dpVZsOy21G96-VAaUS9To9ucCRy4zVZMFRZiBVGFG19lRe26i58DA-dKc1AFTtUj76ATemGeoUh-D-fMNLtM8nom1s7WyjTCss48p96lGGYhP8Re2DImfxHrT2gL3Dy8JyfuKaVIz6gxDLF_I-S46NZKa8Rp9WOkJHE0J7r_nF6v4uUzc9xbjogmn2CBGHvI_Awci1wSKcxGUVHpeiI",
date: "20 Июля 2023",
tags: ["События", "Выставка"],
href: "/news/3"
},
{
id: 4,
title: "Награда за безопасность труда 2023",
description: "Наша компания получила отраслевую награду \"Золотой шлем\" за внедрение передовых стандартов безопасности при погрузочно-разгрузочных работах.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuCNrqfzHL13CiJyE09t9q00eNPGxXzHZmkJOnAJwWb-ZGksjDhJa-pMKLIwdQajZ2H3znwO6Npv4mTCHW6y22Cjk5uGKqm5twaUEqY5516Fz8GDIonYvhf7JFLVSo62yNGjE214kPru2mIXLnWtNnDOYYu_38ISFo0rLXcAnj9OwVyh7pu0vReSDJjuZE7eeTdcsWk6GmDX-0exBgiHP_iEcD4Q0wUgWt6kisHrbhJbjPOaifncmkJ_L8GnZZv2Qqd4Usu2pzROdAg",
date: "05 Июля 2023",
tags: ["Награды", "Безопасность"],
href: "/news/4"
},
{
id: 5,
title: "Открытие нового маршрута: Китай - Россия",
description: "Запущен регулярный сервис доставки сборных грузов через погранпереход Забайкальск. Сроки доставки сокращены на 4 дня.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuAwu8Rfx_hUIGFZWILnPxm8pF3pCHt_INv95Z-da1kb3w5zp2T3eIa3aQ3B7odhyhwA8tHFnG9xQQUdNHuYBxabyEHmPLOw4SeHiUA2j5Kn1Z7gm7-L5Qp14jM8kuNdWF7_Sf1H9_9HrPaYUgZQGMRAG_l1xMtVm5yxlyMC8qQtCCh_QQBI3aF7_kKQrwkzb4TuUNLFgCPZ2E78k581azflIyBZfzRLwuxfur5v60PRTFVEapaiHm_LXr0TcQskucSMSmg0Y-lh_ao",
date: "18 Июня 2023",
tags: ["Маршруты", "Международные"],
href: "/news/5"
},
{
id: 6,
title: "Стратегическое партнерство с \"СибНефтьГаз\"",
description: "Подписан долгосрочный контракт на логистическое обслуживание новых месторождений в Восточной Сибири.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuCdmtBSwT2WN1VKpedinOOHzhmC9qYatDPXtXL6ReeWoZkdBaVdIYk5NQXi73-ves5cnRp2BepSGeAZmFRnsVbTw0NKpg-PLNTfIariKA0CI37zrewHRoLxnWunMA2Qvp1uwN7vXjnlYFFF1rdWhZ783BssxTFtMmRk3c1aq5dV8ntlYLpJnXVBdDfW8TXnO2SOXF-ZtOcygfRioQR2kc6APXP41_hx36QUwqCRc6cAC6vxW0MiEoUoXApjoxWtein_9uoZF-gBbG0",
date: "01 Июня 2023",
tags: ["Партнерство", "Контракты"],
href: "/news/6"
}
];
---
<!-- News Grid -->
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1440px] mx-auto w-full px-4 md:px-10">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
{news.map(item => (
<NewsCard
id={item.id}
title={item.title}
description={item.description}
image={item.image}
date={item.date}
tags={item.tags}
href={item.href}
/>
))}
</div>
</div>
</div>

View file

@ -0,0 +1,10 @@
<!-- Pagination -->
<div class="layout-container flex justify-center w-full">
<div class="layout-content-container flex flex-col max-w-[1200px] mx-auto w-full px-4 lg:px-40">
<div class="flex justify-center mb-16">
<button class="flex min-w-[200px] cursor-pointer items-center justify-center overflow-hidden rounded border border-border-dark bg-surface-dark h-12 px-6 text-white hover:text-background-dark hover:bg-white hover:border-white transition-all text-sm font-bold leading-normal tracking-[0.015em]">
<span class="truncate">Загрузить еще новости</span>
</button>
</div>
</div>
</div>

View file

@ -0,0 +1,15 @@
<!-- CTA Section -->
<div class="max-w-[1440px] mx-auto w-full bg-primary relative overflow-hidden py-16 px-4 sm:px-10">
<div class="absolute inset-0 bg-[#181611] opacity-10" style='background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuB-pTkCCr1vcY38dd7JHGBAg2g5HBPUDlJjlFmSjAaWKBmhUNFPhnAKwxJm1MXy1dooBbFrv8LoC44JUjpEAZ00dDunoRcoe-VXS39dcTqE6_EdI4byNUHkWdsUX8pV2urqqH0WDRGMy-EiN2ipEkEf0vrjZpV3kw61AsQYAriYZCWem6ismHkpBRqXoPr0zDk8cB1-USkpc91N5kckMOl1O0hxRvF_ain8nGf53fjwPdTrs2N-jsHok9HgyuEUWzgeuKXjRiwmxts");'></div>
<div class="max-w-[960px] mx-auto relative z-10 text-center">
<h2 class="text-[#181611] text-3xl md:text-5xl font-black uppercase mb-6 tracking-tight">
Готовы к новому вызову?
</h2>
<p class="text-[#181611] text-lg md:text-xl font-medium mb-8 max-w-2xl mx-auto">
Свяжитесь с нами для расчета вашего проекта. Мы найдем решение для груза любой сложности и габаритов.
</p>
<button class="inline-flex items-center justify-center bg-[#181611] text-white hover:bg-white hover:text-[#181611] px-8 py-4 rounded-sm font-bold uppercase tracking-wide transition-all duration-300 shadow-lg">
Оставить заявку
</button>
</div>
</div>

View file

@ -0,0 +1,20 @@
<!-- Filters -->
<div class="max-w-[1440px] mx-auto w-full px-4 sm:px-10 pb-10 border-b border-border-dark mb-10">
<div class="flex flex-wrap gap-3">
<button class="flex h-10 items-center justify-center px-6 rounded-sm bg-primary text-[#181611] text-sm font-bold uppercase tracking-wide border border-primary">
Все отрасли
</button>
<button class="flex h-10 items-center justify-center px-6 rounded-sm bg-transparent hover:bg-surface-dark text-white text-sm font-medium uppercase tracking-wide border border-border-dark transition-all">
Нефтегаз
</button>
<button class="flex h-10 items-center justify-center px-6 rounded-sm bg-transparent hover:bg-surface-dark text-white text-sm font-medium uppercase tracking-wide border border-border-dark transition-all">
Энергетика
</button>
<button class="flex h-10 items-center justify-center px-6 rounded-sm bg-transparent hover:bg-surface-dark text-white text-sm font-medium uppercase tracking-wide border border-border-dark transition-all">
Строительство
</button>
<button class="flex h-10 items-center justify-center px-6 rounded-sm bg-transparent hover:bg-surface-dark text-white text-sm font-medium uppercase tracking-wide border border-border-dark transition-all">
2023
</button>
</div>
</div>

View file

@ -0,0 +1,50 @@
---
---
<!-- Footer -->
<footer class="bg-[#0f0e0b] border-t border-[#393328] w-full pt-16 pb-8 px-4 sm:px-10">
<div class="max-w-[1440px] mx-auto">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-16">
<div class="flex flex-col gap-6">
<div class="flex items-center gap-3 text-white">
<span class="material-symbols-outlined text-primary text-3xl">local_shipping</span>
<h2 class="text-xl font-bold uppercase">HimTransService</h2>
</div>
<p class="text-[#bab09c] text-sm leading-relaxed">
Экспертные решения в области проектной логистики и негабаритных перевозок. Надежность, подтвержденная годами и сотнями выполненных проектов.
</p>
</div>
<div class="flex flex-col gap-4">
<h4 class="text-white font-bold uppercase tracking-wide mb-2">Навигация</h4>
<a class="text-[#bab09c] hover:text-primary transition-colors text-sm" href="#">О Компании</a>
<a class="text-[#bab09c] hover:text-primary transition-colors text-sm" href="#">Услуги</a>
<a class="text-[#bab09c] hover:text-primary transition-colors text-sm" href="#">Проекты</a>
<a class="text-[#bab09c] hover:text-primary transition-colors text-sm" href="#">Новости</a>
</div>
<div class="flex flex-col gap-4">
<h4 class="text-white font-bold uppercase tracking-wide mb-2">Контакты</h4>
<a class="text-white text-lg font-bold hover:text-primary transition-colors" href="tel:+78000000000">+7 (800) 000-00-00</a>
<a class="text-[#bab09c] hover:text-primary transition-colors text-sm" href="mailto:info@himtrans.ru">info@himtrans.ru</a>
<p class="text-[#bab09c] text-sm">г. Москва, ул. Индустриальная, 42</p>
</div>
<div class="flex flex-col gap-4">
<h4 class="text-white font-bold uppercase tracking-wide mb-2">Следите за нами</h4>
<div class="flex gap-4">
<a class="w-10 h-10 rounded-sm bg-[#181611] border border-[#393328] flex items-center justify-center text-white hover:bg-primary hover:text-[#181611] hover:border-primary transition-all" href="#">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewbox="0 0 24 24"><path clip-rule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" fill-rule="evenodd"></path></svg>
</a>
<a class="w-10 h-10 rounded-sm bg-[#181611] border border-[#393328] flex items-center justify-center text-white hover:bg-primary hover:text-[#181611] hover:border-primary transition-all" href="#">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewbox="0 0 24 24"><path clip-rule="evenodd" d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772 4.902 4.902 0 011.772-1.153c.636-.247 1.363-.416 2.427-.465 1.067-.047 1.409-.06 3.809-.06h.63zm1.418 2.003l-.31.004c-2.422.006-2.839.019-3.796.063-.978.045-1.51.204-1.866.342-.465.18-.797.398-1.15.748-.35.353-.566.683-.748 1.15-.139.356-.297.888-.342 1.867-.044.956-.057 1.372-.063 3.795v.31c.006 2.423.019 2.839.063 3.796.045.979.204 1.511.342 1.867.18.464.398.797.75 1.149.352.352.684.567 1.15.75.355.138.887.296 1.866.342.957.044 1.373.057 3.796.063h.31c2.423-.006 2.84-.019 3.797-.063.978-.045 1.51-.204 1.866-.342.464-.18.797-.398 1.15-.749.352-.352.566-.684.749-1.15.138-.356.296-.888.342-1.867.044-.957.057-1.373.063-3.796l-.004-.31c-.005-2.422-.018-2.839-.063-3.796-.044-.979-.203-1.51-.341-1.867-.18-.465-.398-.797-.749-1.15-.352-.352-.684-.566-1.15-.749-.355-.138-.888-.296-1.866-.341-.957-.044-1.373-.057-3.796-.063zM12 5.838a6.162 6.162 0 110 12.324 6.162 6.162 0 010-12.324zm0 1.933a4.23 4.23 0 100 8.46 4.23 4.23 0 000-8.46zm6.406-3.845a1.44 1.44 0 110 2.88 1.44 1.44 0 010-2.88z" fill-rule="evenodd"></path></svg>
</a>
</div>
</div>
</div>
<div class="border-t border-[#393328] pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p class="text-[#bab09c] text-xs uppercase tracking-wide">© 2023 HimTransService. Все права защищены.</p>
<div class="flex gap-6">
<a class="text-[#bab09c] hover:text-white text-xs uppercase transition-colors" href="#">Политика конфиденциальности</a>
<a class="text-[#bab09c] hover:text-white text-xs uppercase transition-colors" href="#">Карта сайта</a>
</div>
</div>
</div>
</footer>

View file

@ -0,0 +1,28 @@
---
---
<header class="sticky top-0 z-50 w-full bg-[#181611] border-b border-[#393328]">
<div class="max-w-[1440px] mx-auto px-4 sm:px-10 py-3 flex items-center justify-between">
<div class="flex items-center gap-4 text-white">
<div class="size-8 flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-3xl">local_shipping</span>
</div>
<h2 class="text-white text-xl font-bold leading-tight tracking-[-0.015em] uppercase">HimTransService</h2>
</div>
<div class="hidden lg:flex flex-1 justify-end gap-8">
<div class="flex items-center gap-9">
<a class="text-white hover:text-primary transition-colors text-sm font-medium uppercase tracking-wider" href="#">Компания</a>
<a class="text-white hover:text-primary transition-colors text-sm font-medium uppercase tracking-wider" href="#">Автопарк</a>
<a class="text-primary text-sm font-bold uppercase tracking-wider underline decoration-2 underline-offset-4" href="#">Проекты</a>
<a class="text-white hover:text-primary transition-colors text-sm font-medium uppercase tracking-wider" href="#">Контакты</a>
</div>
<button class="flex cursor-pointer items-center justify-center overflow-hidden rounded-sm h-10 px-6 bg-primary hover:bg-yellow-500 transition-colors text-[#181611] text-sm font-bold uppercase tracking-wide">
<span class="truncate">Расчет стоимости</span>
</button>
</div>
<!-- Mobile Menu Icon -->
<button class="lg:hidden text-white">
<span class="material-symbols-outlined">menu</span>
</button>
</div>
</header>

View file

@ -0,0 +1,9 @@
<!-- Hero Title Section -->
<div class="max-w-[1440px] mx-auto w-full px-4 sm:px-10 pb-8">
<h1 class="text-white text-5xl md:text-7xl font-black uppercase tracking-tighter leading-none mb-6">
Выполненные<br/><span class="text-transparent bg-clip-text bg-gradient-to-r from-primary to-yellow-200">Проекты</span>
</h1>
<p class="text-[#bab09c] text-lg max-w-2xl font-light">
Портфолио сложных логистических решений по перевозке негабаритных и тяжеловесных грузов по России и странам СНГ. Мы берем на себя ответственность там, где другие видят препятствия.
</p>
</div>

View file

@ -0,0 +1,134 @@
---
const projects = [
{
id: 1,
title: "Транспортировка Газовой Турбины ГТЭ-160",
description: "Мультимодальная перевозка с использованием речной баржи и модульных тралов. Преодоление узких мостовых пролетов и сложного рельефа местности в условиях ограниченного времени.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuBozMUtPuxjJYMGy5ICJT8MZCAjqNP0RZh_xBASkCgEg04xtao0Q54qqaCU4q7HfAVoleDiNHw-_YPf5bkjnypHf5l2QEF0s2xFJx4MS4dN2aePPqQ-hOroSNvXqYZy-BaReyMvNc6T0LENG5prp-IRyT2K4Ho4Srs6iPKyPQhGha1Qr1JFNrpmYylP2da7e5gyKhHESpVSCKfW-26I720gsBGKcYCVWrcSz3liH6PucT6AI5PgjYzNSYRBQGUpBP4CH5r1BBSMLy4",
category: "Энергетика",
date: "Октябрь 2023",
location: "Тобольск",
weight: "185 т",
distance: "2 400 км",
equipment: "Goldhofer 12 осей",
projectId: "HTS-23-089"
},
{
id: 2,
title: "Перевозка Колонны Гидрокрекинга",
description: "Уникальная логистическая операция по доставке сверхгабаритного оборудования на НПЗ. Потребовалось временное перекрытие федеральной трассы и демонтаж линий электропередач.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuCTRLWft4h301qTy13uuD6dmeRXpbokNfgjKIFSXVw-eYj5laekOGh58WHFsv_SU0mNr8_DYKvTkv8HkFcd_Ydlm2_sZWhlqkdGWTDCMsKM--ZefNh0ZcLGMR_dsowOvENPEvirsorZqjJWCb2XO_hQknTPIzgnNk7gPIqy4VxlmU_vhpoNgXEvWW2KXSz-L4lkK1BWykz-8k4ABffLa1tnX-fWoc0HGVP2AVkkSrbL6MxnnuMcoTWw-hsxdKqOLGs9VHPvHd8YAlY",
category: "Нефтехимия",
date: "Август 2023",
location: "Омск",
weight: "320 т",
length: "54 м",
team: "18 чел.",
projectId: "HTS-23-042"
},
{
id: 3,
title: "Перебазировка Парка Карьерной Техники",
description: "Срочная переброска 12 единиц тяжелой карьерной техники Liebherr и Komatsu в условиях вечной мерзлоты и отсутствия дорожной инфраструктуры.",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuDE-wSMoUDOW2HFa3kJnmz2FoLVcdQ4HpohPufXKNcsq2nzmdgKAblAxkNKgy9I1hPOkvHgScoF3LUSyk_NdViaDrykd5WhBrDZVACkF6CsJrj9VbyPflgoITK0j124AexNQ8pub-wLda-L4uX0_LXDMZ0A6-z4VinjJKq9ugnJz8Aalp1YXr_vFwhz1DVVrVULIDlbYAsYlOBEriPsqMT5Sb-kXtg6JL2K6pYYNVzNdD4baXHDbxBx4venjz3gZywCzRcFgdjFVZg",
category: "Строительство",
date: "Июнь 2023",
location: "Норильск",
totalWeight: "850 т",
units: "12 шт",
duration: "7 дней",
projectId: "HTS-23-021"
}
];
---
<!-- Projects Grid -->
<div class="max-w-[1440px] mx-auto w-full px-4 sm:px-10 pb-20 flex flex-col gap-16">
{projects.map(project => (
<article class="group relative flex flex-col lg:flex-row bg-surface-dark border border-border-dark rounded-sm overflow-hidden hover:border-primary/50 transition-colors duration-300">
{/* Image Section */}
<div class="lg:w-1/2 relative min-h-[300px] lg:min-h-[450px] overflow-hidden">
<div class="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-105" data-alt={project.title} style={`background-image: url("${project.image}");`}></div>
<div class="absolute inset-0 bg-gradient-to-t from-[#181611] via-transparent to-transparent opacity-60"></div>
<div class="absolute top-4 left-4 bg-primary text-[#181611] px-3 py-1 text-xs font-bold uppercase rounded-sm">
{project.category}
</div>
</div>
{/* Content Section */}
<div class="lg:w-1/2 p-6 lg:p-10 flex flex-col justify-between relative">
{/* Project Info */}
<div>
<div class="flex items-center gap-2 mb-4 text-[#bab09c] text-sm font-mono uppercase tracking-wider">
<span class="material-symbols-outlined text-base">calendar_month</span> {project.date}
<span class="mx-2">|</span>
<span class="material-symbols-outlined text-base">location_on</span> {project.location}
</div>
<h3 class="text-white text-2xl lg:text-3xl font-bold uppercase leading-tight mb-4 group-hover:text-primary transition-colors">
{project.title}
</h3>
<p class="text-[#bab09c] text-base leading-relaxed mb-8 border-l-2 border-primary/30 pl-4">
{project.description}
</p>
{/* Tech Specs Grid */}
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-8">
{project.weight && (
<div class="bg-[#181611] p-3 rounded-sm border border-border-dark">
<p class="text-[#bab09c] text-xs uppercase mb-1">Вес груза</p>
<p class="text-white text-lg font-bold">{project.weight}</p>
</div>
)}
{project.distance && (
<div class="bg-[#181611] p-3 rounded-sm border border-border-dark">
<p class="text-[#bab09c] text-xs uppercase mb-1">Расстояние</p>
<p class="text-white text-lg font-bold">{project.distance}</p>
</div>
)}
{project.equipment && (
<div class="bg-[#181611] p-3 rounded-sm border border-border-dark col-span-2 md:col-span-1">
<p class="text-[#bab09c] text-xs uppercase mb-1">Техника</p>
<p class="text-white text-lg font-bold">{project.equipment}</p>
</div>
)}
{project.length && (
<div class="bg-[#181611] p-3 rounded-sm border border-border-dark">
<p class="text-[#bab09c] text-xs uppercase mb-1">Длина</p>
<p class="text-white text-lg font-bold">{project.length}</p>
</div>
)}
{project.team && (
<div class="bg-[#181611] p-3 rounded-sm border border-border-dark">
<p class="text-[#bab09c] text-xs uppercase mb-1">Команда</p>
<p class="text-white text-lg font-bold">{project.team}</p>
</div>
)}
{project.totalWeight && (
<div class="bg-[#181611] p-3 rounded-sm border border-border-dark">
<p class="text-[#bab09c] text-xs uppercase mb-1">Общий вес</p>
<p class="text-white text-lg font-bold">{project.totalWeight}</p>
</div>
)}
{project.units && (
<div class="bg-[#181611] p-3 rounded-sm border border-border-dark">
<p class="text-[#bab09c] text-xs uppercase mb-1">Единиц</p>
<p class="text-white text-lg font-bold">{project.units}</p>
</div>
)}
{project.duration && (
<div class="bg-[#181611] p-3 rounded-sm border border-border-dark col-span-2 md:col-span-1">
<p class="text-[#bab09c] text-xs uppercase mb-1">Срок</p>
<p class="text-white text-lg font-bold">{project.duration}</p>
</div>
)}
</div>
</div>
{/* Action */}
<div class="pt-6 border-t border-border-dark flex justify-between items-center">
<span class="text-white text-xs uppercase tracking-widest opacity-50 font-mono">Project ID: #{project.projectId}</span>
<button class="flex items-center gap-2 text-primary font-bold uppercase tracking-wider hover:text-white transition-colors group/btn">
Изучить кейс
<span class="material-symbols-outlined group-hover/btn:translate-x-1 transition-transform">arrow_forward</span>
</button>
</div>
</div>
</article>
))}
</div>

View file

@ -0,0 +1,81 @@
---
// src/components/sections/ServicesGrid.astro
const services = [
{
title: "Автоперевозки",
icon: "local_shipping",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuBMWUfDDchCKK_m3DBflFPF9-53SNEVnKJcX8DEXdhbsTFp70l1QHuF6-WGS0kxKktJCm8KrE4Ri4wLLDoZGCs7xmRVJRGflyciuxATksnQ2uzIjLwzkq5Ul5iwUevxqG48jU9YX52Hay-iW0MGEXb-SThjnvUxsH-j0HCwAP03PQ4BttK9h4JNYxFOkPDvxta9RNZkq8zlzLx5GLk61ba5PQTzjV0OBhiTOzKrEYqpKb0YRX74afUXWg6dx7YKt06jB2lC1R5iAl8",
description: "Транспортировка негабарита с использованием собственных низкорамных тралов и модульных прицепов.",
tags: ["Тралы до 120т", "Разрешения"],
isPrimary: true
},
{
title: "Морской фрахт",
icon: "directions_boat",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuDyohaoIQE3MQBIIhgNna4rqcWQonPYg6aj6aK2cIL_BKlv7rtxrjJvEjeeVkSs8Mcl_nzELtqXe5vxrm-v71YRxhf7knhFjAHmCPV1yhvAZhYIXcJpG3sps_wDm2wI1ulXfKoEqHwRokovZhvft2jgQK9KauqBlkusn49l3uOLm2uR_oK5eGfdSs0o3kAlCpvWrJxePmqMLo4D0fvjt2z3Z2mEvXXzAt20oZ9zvw3VAyZGf3HsC_zvEkgDTcdebIium7lHuj0uRuM",
description: "Чартерные перевозки, внутрипортовое экспедирование и перевалка грузов в портах РФ и мира.",
tags: ["Break Bulk", "Портовые работы"]
},
{
title: "Ж/Д Перевозки",
icon: "train",
image: "https://lh3.googleusercontent.com/aida-public/AB6AXuArlmfYvzJVHYmPWTtzWy1gv4nSdm9QYBFuDx3Y8USZ9VtwxnTPCoJqMh88jjo22akATh7mVtlF-i3nYF10-Qj-hYO-hwLgEXXziNzmUz14lRC7kNn8_X0GVIH5scP71ao0X7urnG2sgHyEERw3vDQDwRAk99ZDKy5UMO-Pkvno6Oq5zBKN0dwpxGnM1PrQeU6ZtCdtC1f0rDUoiudO5d4356-b17bt5eJu7vdo4MSShe9kwWaEztVadKgYidZWw62zZaBu4qtuVPM",
description: "Организация отправки грузов на транспортерах и платформах. Разработка схем крепления.",
tags: ["Схемы НТУ", "Транспортеры"]
}
];
const secondaryServices = [
{ title: "Проектная логистика", icon: "engineering", desc: "Управление сложными цепочками поставок под ключ." },
{ title: "Охрана и сопровождение", icon: "shield_person", desc: "Предоставление машин прикрытия (PILOT) и патрулей ГИБДД." },
{ title: "Таможенное оформление", icon: "description", desc: "Подготовка полного пакета документов для ВЭД." }
];
---
<section class="w-full py-16 px-4 sm:px-6 lg:px-8 bg-[#221c10]">
<div class="max-w-[1440px] mx-auto">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Основные услуги с картинками -->
{services.map(item => (
<div class="group relative flex flex-col h-full bg-[#181611] border border-[#393328] hover:border-primary transition-all duration-300 overflow-hidden">
<div class="h-64 overflow-hidden relative">
<div class="absolute inset-0 bg-primary/20 z-10 mix-blend-multiply transition-opacity group-hover:opacity-0"></div>
<div class="absolute inset-0 bg-gradient-to-t from-[#181611] to-transparent z-20"></div>
<img src={item.image} alt={item.title} class="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500 transform group-hover:scale-105" />
</div>
<div class="p-8 flex flex-col flex-1 relative z-30 -mt-12">
<div class:list={["size-14 flex items-center justify-center mb-6 shadow-lg", item.isPrimary ? "bg-primary text-[#181611]" : "bg-[#2a2a2a] text-primary border border-primary group-hover:bg-primary group-hover:text-[#181611] transition-colors"]}>
<span class="material-symbols-outlined !text-[32px]">{item.icon}</span>
</div>
<h3 class="text-2xl font-bold text-white uppercase mb-3">{item.title}</h3>
<p class="text-gray-400 mb-6 flex-1 text-sm leading-relaxed">{item.description}</p>
<ul class="flex flex-wrap gap-2 mb-8">
{item.tags.map(tag => (
<li class="text-xs font-bold text-primary bg-primary/10 px-2 py-1 uppercase tracking-wider">{tag}</li>
))}
</ul>
<a href="#" class="inline-flex items-center gap-2 text-white font-bold uppercase tracking-wider text-sm hover:text-primary transition-colors group/link">
Подробнее <span class="material-symbols-outlined text-lg group-hover/link:translate-x-1 transition-transform">arrow_forward</span>
</a>
</div>
</div>
))}
<!-- Дополнительные услуги (без картинок) -->
{secondaryServices.map(item => (
<div class="group relative flex flex-col h-full bg-[#181611] border border-[#393328] hover:border-primary transition-all duration-300 p-8">
<div class="size-14 bg-[#2a2a2a] text-white flex items-center justify-center mb-6 border border-white/10 group-hover:border-primary transition-colors">
<span class="material-symbols-outlined !text-[32px]">{item.icon}</span>
</div>
<h3 class="text-2xl font-bold text-white uppercase mb-3">{item.title}</h3>
<p class="text-gray-400 mb-6 flex-1 text-sm leading-relaxed">{item.desc}</p>
<a href="#" class="inline-flex items-center gap-2 text-primary font-bold uppercase tracking-wider text-sm group/link">
Подробнее <span class="material-symbols-outlined text-lg group-hover/link:translate-x-1 transition-transform">arrow_forward</span>
</a>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,21 @@
<section class="relative w-full py-20 px-4 sm:px-6 lg:px-8 bg-[#181611] border-b border-[#393328]">
<div class="max-w-[1440px] mx-auto">
<div class="flex flex-col gap-6 max-w-4xl">
<div class="flex items-center gap-2 text-primary font-bold tracking-widest uppercase text-sm">
<span class="w-8 h-[2px] bg-primary block"></span>
<span>Специализация</span>
</div>
<h1 class="text-4xl md:text-6xl lg:text-7xl font-bold uppercase leading-tight tracking-tight text-white">
Project Cargo <br/>
<span class="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-600">Логистика</span>
</h1>
<p class="text-gray-400 text-lg md:text-xl max-w-2xl font-light">
Комплексные решения по перевозке негабаритных, тяжеловесных и проектных грузов. Мы берем на себя ответственность за самые сложные маршруты.
</p>
</div>
</div>
<!-- Abstract background -->
<div class="absolute right-0 top-0 h-full w-1/3 opacity-10 pointer-events-none hidden lg:block"
style="background: repeating-linear-gradient(45deg, #f2a60d, #f2a60d 1px, transparent 1px, transparent 20px);">
</div>
</section>

View file

@ -0,0 +1,21 @@
---
// src/components/sections/Stats.astro
const stats = [
{ value: "15+", label: "Лет опыта" },
{ value: "2М+", label: "Тонн груза" },
{ value: "500+", label: "Проектов" },
{ value: "100%", label: "Страхование" },
];
---
<section class="w-full bg-[#181611] border-t border-[#393328] py-12">
<div class="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
{stats.map(s => (
<div class="flex flex-col gap-1 border-l-2 border-primary pl-4">
<span class="text-3xl md:text-4xl font-bold text-white">{s.value}</span>
<span class="text-sm text-gray-400 uppercase tracking-wider">{s.label}</span>
</div>
))}
</div>
</div>
</section>

View file

@ -0,0 +1,64 @@
export const SITE = {
// Основная информация о сайте
TITLE: "ХимТрансСервис",
DESCRIPTION: "ХимТрансСервис - надежные негабаритные грузоперевозки. Профессиональные услуги транспортировки по всей России и СНГ.",
TAGLINE: "Профессиональные негабаритные грузоперевозки по России и СНГ. Мы доставляем то, что другие не могут поднять.",
AUTHOR: "ХимТрансСервис",
// Контактная информация
CONTACTS: {
footer: [
{ icon: "location_on", text: "г. Самара, ул. Товарная 70" },
{ icon: "call", text: "+79109245151" },
{ icon: "mail", text: "htsbuch63@yandex.ru" }
],
page: [
{
label: "Адрес офиса",
icon: "location_on",
value: "г. Самара, ул. Товарная 70",
isHtml: false
},
{
label: "Телефон",
icon: "call",
value: "+79109245151",
subValue: "Пн-Пт: 09:00 - 18:00",
link: "tel:+79109245151"
},
{
label: "Email",
icon: "mail",
emails: ["htsbuch63@yandex.ru"]
}
]
} as const,
// URL сайта
URL: "https://hts.ru",
// Основные страницы
PAGES: {
HOME: "https://hts.ru",
SERVICES: "https://hts.ru/services",
ABOUT: "https://hts.ru/about",
PROJECTS: "https://hts.ru/projects",
CONTACT: "https://hts.ru/contacts",
CARS: "https://hts.ru/cars",
BLOG: "https://hts.ru/blog",
NEWS: "https://hts.ru/news",
ERROR_404: "https://hts.ru/404",
},
// Описания для основных страниц
PAGE_DESCRIPTIONS: {
HOME: "ХимТрансСервис - надежные негабаритные грузоперевозки. Профессиональные услуги транспортировки по всей России и СНГ.",
SERVICES: "Узнайте о наших услугах по транспортировке негабаритных грузов. ХимТрансСервис предоставляет профессиональные услуги грузоперевозок по всей России и СНГ.",
ABOUT: "Узнайте больше о компании ХимТрансСервис - надежном партнере в сфере грузоперевозок.",
CONTACT: "Свяжитесь с нами для получения дополнительной информации о наших услугах.",
PROJECTS: "Наши реализованные проекты по негабаритным перевозкам. Узнайте о наших успешных проектах в сфере грузоперевозок.",
CARS: "Узнайте о нашем автопарке ХимТрансСервис. Современные грузовики для перевозки любых грузов.",
BLOG: "Читайте последние новости и статьи от ХимТрансСервис. Полезная информация о грузоперевозках, логистике и индустрии.",
NEWS: "Последние обновления, события и объявления от ХимТрансСервис. Читайте новости компании и отрасли.",
}
} as const;

View file

@ -0,0 +1,21 @@
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const postCollection = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/post" }),
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.string(),
coverImage: z.object({
src: z.string(),
alt: z.string(),
}),
author: z.string(),
tags: z.array(z.string()).max(3, { message: "Допускается не более 3 тегов" }).optional(),
}),
});
export const collections = {
post: postCollection,
};

View file

@ -0,0 +1,56 @@
---
title: "5 главных преимуществ фреймворка Astro"
description: "Узнайте о ключевых преимуществах Astro, которые делают его идеальным выбором для современных веб-проектов."
publishDate: "2025-08-25"
author: "Sergey"
tags: ["Astro", "Производительность", "Веб-разработка"]
coverImage:
src: "/images/posts/2025/08/astro-adv.avif"
alt: "Абстрактный код на холсте"
---
## Введение
В мире постоянно развивающихся веб-технологий выбор правильного фреймворка имеет решающее значение для успеха проекта.
Astro — это относительно новый, но быстро набирающий популярность статический генератор сайтов, который предлагает уникальный подход к созданию быстрых, SEO-оптимизированных и гибких веб-приложений. В этой статье мы рассмотрим пять ключевых преимуществ Astro, которые выделяют его среди конкурентов и делают отличным выбором для современных разработчиков.
## 1. Островная архитектура для максимальной производительности
Astro использует революционную “островную” архитектуру (Islands Architecture), которая является его визитной карточкой. Этот подход кардинально отличается от традиционных SPA-фреймворков, которые обычно отправляют на клиентскую сторону большой объем JavaScript.
Вместо этого Astro по умолчанию генерирует чистый HTML и CSS. Интерактивные компоненты, которые мы называем “островами”, гидрируются (становятся интерактивными) только тогда, когда это действительно необходимо, и только в тех конкретных местах страницы, где они расположены.
Такой подход минимизирует объем JavaScript, загружаемого браузером. Результатом является молниеносная загрузка страниц, значительное улучшение показателей Core Web Vitals (такие как LCP, FID, CLS) и обеспечение превосходного пользовательского опыта, особенно на мобильных устройствах и при медленном соединении.
## 2. Гибкость и независимость от UI-фреймворков
Одним из самых мощных преимуществ Astro является его агностицизм по отношению к UI-фреймворкам. Вы не привязаны к React, Vue, Svelte или любому другому фреймворку. Astro позволяет использовать компоненты, написанные на React, Vue, Svelte, Solid, Lit, Preact или даже чистом HTML/CSS/JavaScript, в одном проекте.
Это дает разработчикам беспрецедентную свободу выбора и позволяет использовать лучшие инструменты для каждой конкретной задачи. Например, вы можете использовать React для сложных интерактивных виджетов, а Svelte для более простых компонентов, при этом основная часть страницы будет статическим HTML. Это также упрощает постепенную миграцию существующих проектов, позволяя интегрировать Astro без полного переписывания кодовой базы.
## 3. Отличная SEO-оптимизация из коробки
Поскольку Astro по умолчанию генерирует статический HTML, он обеспечивает превосходную индексацию поисковыми системами. Весь контент доступен краулерам сразу, без необходимости выполнения JavaScript на стороне клиента для рендеринга содержимого.
Это критически важно для блогов, маркетинговых сайтов, новостных порталов и любых проектов, где SEO играет ключевую роль в привлечении трафика. Быстрая загрузка страниц и чистый HTML также положительно влияют на ранжирование в поисковых системах, что делает Astro идеальным выбором для контент-ориентированных веб-сайтов.
## 4. Простота разработки и отличный DX (Developer Experience)
Astro разработан с учетом удобства разработчиков и предлагает отличный DX. Он предоставляет интуитивно понятный синтаксис, который легко освоить, особенно если вы уже знакомы с HTML, CSS и JavaScript.
Встроенная поддержка TypeScript, быстрая перезагрузка в режиме разработки (Hot Module Replacement) и мощная, но простая в использовании система плагинов значительно ускоряют процесс разработки. Astro минимизирует время на настройку и отладку, позволяя разработчикам сосредоточиться на создании функциональности, а не на борьбе с конфигурацией.
Это делает его привлекательным как для новичков, так и для опытных команд.
## 5. Встроенная поддержка Markdown и MDX
Для контент-ориентированных сайтов, таких как блоги, документация или портфолио, Astro предлагает первоклассную встроенную поддержку Markdown и MDX (Markdown с JSX). Это значительно упрощает создание и управление контентом.
Вы можете писать статьи в привычном формате Markdown, а при необходимости легко встраивать интерактивные компоненты React, Vue или Svelte прямо в ваши статьи с помощью MDX. Это позволяет создавать динамичный и интерактивный контент без необходимости использования сложных CMS или дополнительных инструментов, делая процесс публикации быстрым и эффективным.
## Заключение
В заключение, Astro представляет собой мощный и гибкий фреймворк, который идеально подходит для создания высокопроизводительных, SEO-оптимизированных и современных веб-сайтов.
Его уникальная архитектура, агностицизм к UI-фреймворкам, отличная поддержка SEO, удобство для разработчиков и встроенная поддержка Markdown/MDX делают его привлекательным выбором для широкого круга проектов. Если вы ищете способ создавать быстрые и эффективные веб-приложения, Astro определенно заслуживает вашего внимания.

View file

@ -0,0 +1,64 @@
---
title: "Astro и Payload CMS: идеальный выбор для контентных сайтов"
description: "Узнайте, почему сочетание сверхбыстрого фронтенд-фреймворка Astro и гибкой headless CMS Payload создает идеальную платформу для современных сайтов."
publishDate: "2025-08-25"
author: "Sergey"
tags: ["Astro", "Payload CMS", "Headless CMS"]
coverImage:
src: "/images/posts/2025/08/astro-payloadcms.avif"
alt: "Схема, показывающая связь между системами"
---
## Введение
В современной веб-разработке выбор правильного стека технологий определяет не только скорость и функциональность сайта, но и удобство работы для разработчиков и контент-менеджеров. Когда речь заходит о создании контент-ориентированных сайтов — блогов, новостных порталов, портфолио или корпоративных сайтов — на первый план выходят два ключевых требования: высочайшая производительность и гибкость в управлении контентом. Именно здесь связка **Astro** и **Payload CMS** проявляет себя как идеальное решение.
## Что такое Astro?
**Astro** — это современный веб-фреймворк, созданный для построения сверхбыстрых сайтов. Его ключевая особенность — **архитектура островов (Islands Architecture)**. Astro рендерит весь сайт в статический HTML на сервере во время сборки, отправляя в браузер ноль JavaScript по умолчанию. Интерактивные элементы (UI-компоненты) загружаются как изолированные "острова", что позволяет достичь почти идеальных показателей в Google PageSpeed Insights без лишних усилий.
## Что такое Payload CMS?
**Payload CMS** — это headless CMS нового поколения, написанная на TypeScript. В отличие от традиционных CMS, Payload предоставляет контент через API, что дает разработчикам полную свободу в выборе фронтенд-технологий.
Ключевые преимущества Payload:
- **Гибкость:** Вы можете определить любую структуру контента с помощью мощной системы полей.
- **Developer-first:** CMS настраивается кодом, что позволяет легко версионировать конфигурацию и интегрировать ее в любой CI/CD процесс.
- **Расширяемость:** Payload построена на Express.js, что позволяет легко добавлять кастомные эндпоинты и логику.
- **Современный UI:** Административная панель построена на React, она быстрая, интуитивно понятная и легко кастомизируется.
## Идеальное сочетание: почему они так хорошо работают вместе?
Когда вы объединяете Astro и Payload, вы получаете лучшее из двух миров.
### 1. Непревзойденная производительность
Astro генерирует статичные страницы, которые загружаются мгновенно. Данные для этих страниц он получает из Payload CMS во время сборки. Пользователь получает готовый HTML, а поисковые роботы — идеально оптимизированный для индексации сайт.
### 2. Гибкость и контроль над контентом
Payload позволяет создать именно ту структуру данных, которая нужна вашему проекту. Больше не нужно подстраиваться под ограничения CMS. Вы можете создавать сложные связи между коллекциями, глобальные блоки контента (например, для хедера и футера) и многое другое. Astro легко "подхватывает" эти данные через API.
### 3. Превосходный опыт для разработчиков (Developer Experience)
- **Astro** позволяет использовать компоненты на любом популярном фреймворке (React, Vue, Svelte), если это необходимо для интерактивности.
- **Payload** настраивается кодом, что близко и понятно любому разработчику.
- Получение данных в Astro из Payload — это простой `fetch`-запрос к API.
Пример получения постов в Astro:
```typescript
import type { Post } from '../types';
const response = await fetch(`${import.meta.env.PAYLOAD_API_URL}/api/posts`);
const data = await response.json();
const posts: Post[] = data.docs;
```
### 4. Масштабируемость и готовность к будущему
Декаплированная (decoupled) архитектура, где фронтенд и бэкенд полностью независимы, — это современный стандарт. Вы можете изменять и обновлять фронтенд на Astro, не затрагивая CMS. И наоборот, развивать бэкенд, не боясь сломать отображение.
## Заключение
Связка **Astro + Payload CMS** — это мощный, современный и гибкий стек для создания контентных сайтов. Astro обеспечивает высочайшую скорость загрузки и превосходное SEO, а Payload CMS дает полный контроль над контентом и непревзойденное удобство для разработчиков. Если вы хотите создать сайт, который будет радовать и пользователей, и вашу команду разработки, обязательно присмотритесь к этой комбинации.

View file

@ -0,0 +1,181 @@
---
title: "Astro + Payload CMS: Создаем молниеносный сайт с нуля. Пошаговое руководство"
description: "Пошаговое руководство по созданию сверхбыстрого блога с использованием Astro для фронтенда и Payload CMS для управления контентом."
publishDate: "2025-08-25"
author: "Sergey"
tags: ["Astro", "Payload CMS", "Интеграция"]
coverImage:
src: "/images/posts/2025/08/astro_payload_guide.avif"
alt: "Рабочее место разработчика со старинным компьютером"
---
## Введение
Создание современного сайта требует скорости, гибкости и удобного управления контентом. В этом руководстве мы с нуля создадим простой, но очень быстрый блог, используя две мощные технологии: **Astro** для фронтенда и **Payload CMS** в качестве headless CMS. Это пошаговое руководство проведет вас через все этапы: от инициализации проектов до вывода постов из CMS на страницах сайта.
### Предварительные требования
Перед началом убедитесь, что на вашем компьютере установлен **Node.js** (версии 18 или выше) и пакетный менеджер, например, **pnpm** или **npm**.
---
## Шаг 1: Настройка Payload CMS
Сначала настроим бэкенд — нашу систему управления контентом.
1. **Создайте проект Payload.**
Откройте терминал и выполните команду:
```bash
npx create-payload-app my-blog-backend
```
В процессе установки выберите `TypeScript` и шаблон `Blank` (пустой).
2. **Определите коллекцию постов.**
Payload управляет контентом через "коллекции". Создадим коллекцию для наших постов. Создайте файл `src/collections/Posts.ts` и добавьте в него следующий код:
```typescript
import { CollectionConfig } from 'payload/types';
const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
},
{
name: 'content',
type: 'richText',
},
],
};
export default Posts;
```
3. **Подключите коллекцию в конфигурации Payload.**
Откройте файл `src/payload.config.ts` и добавьте вашу новую коллекцию в массив `collections`:
```typescript
import { buildConfig } from 'payload/config';
import Posts from './collections/Posts';
import Users from './collections/Users';
export default buildConfig({
// ... остальная конфигурация
collections: [
Users,
Posts, // <-- Добавляем нашу коллекцию сюда
],
// ...
});
```
4. **Запустите Payload.**
Перейдите в директорию проекта (`cd my-blog-backend`) и запустите сервер разработки:
```bash
pnpm dev
```
При первом запуске Payload предложит создать администратора. После этого вы сможете зайти в админ-панель по адресу `http://localhost:3000/admin` и создать свой первый пост. Не забудьте заполнить все поля, включая `slug`.
---
## Шаг 2: Настройка Astro
Теперь займемся фронтендом.
1. **Создайте проект Astro.**
Откройте новый терминал и выполните команду:
```bash
pnpm create astro@latest my-blog-frontend
```
Выберите шаблон `Empty` (пустой) и `TypeScript` (`Strict`).
2. **Базовая структура.**
Astro уже создал для вас базовую структуру. Основные файлы, с которыми мы будем работать, находятся в `src/pages/`.
---
## Шаг 3: Соединяем Astro и Payload
Магия происходит здесь. Мы будем запрашивать данные из API нашего Payload CMS во время сборки сайта Astro.
1. **Создаем страницу для вывода всех постов.**
Создайте файл `src/pages/blog/index.astro`. Этот файл будет запрашивать список всех постов и отображать их.
```astro
---
import Layout from '../../layouts/Layout.astro';
// Запрос к API Payload для получения всех постов
const response = await fetch('http://localhost:3000/api/posts');
const data = await response.json();
const posts = data.docs;
---
<Layout title="Мой блог">
<h1>Все посты</h1>
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
</Layout>
```
2. **Создаем динамическую страницу для одного поста.**
Astro позволяет создавать динамические маршруты. Создайте файл `src/pages/blog/[slug].astro`. Он будет генерировать отдельную страницу для каждого поста.
```astro
---
import Layout from '../../layouts/Layout.astro';
export async function getStaticPaths() {
const response = await fetch('http://localhost:3000/api/posts');
const data = await response.json();
const posts = data.docs;
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
---
<Layout title={post.title}>
<h1>{post.title}</h1>
<div>
<!-- Payload RichText хранится в формате, который нужно будет распарсить -->
<!-- Для простоты пока выведем как JSON -->
<pre>{JSON.stringify(post.content, null, 2)}</pre>
</div>
</Layout>
```
## Шаг 4: Запуск и проверка
Перейдите в директорию фронтенда (`cd my-blog-frontend`) и запустите сервер разработки Astro:
```bash
pnpm dev
```
Теперь откройте `http://localhost:4321/blog` в вашем браузере. Вы должны увидеть список постов, которые вы создали в Payload. Кликнув по ссылке, вы перейдете на страницу конкретного поста.
## Заключение
Поздравляем! Вы только что создали основу для молниеносного сайта с разделенными фронтендом и бэкендом. Вы использовали Payload CMS для удобного управления контентом и Astro для его максимально быстрой доставки пользователю.
Это только начало. Теперь вы можете добавлять стили с помощью Tailwind CSS, усложнять структуру контента в Payload и расширять функциональность вашего Astro-сайта.

View file

@ -0,0 +1,64 @@
---
title: "Payload CMS для Astro: Почему это может быть лучше, чем Strapi или Contentful"
description: "Сравнительный анализ Payload CMS, Strapi и Contentful. Узнайте, почему Payload может стать лучшим выбором для вашего следующего проекта на Astro."
publishDate: "2025-08-28"
author: "Sergey"
tags: ["Headless CMS", "Astro", "Сравнение"]
coverImage:
src: "/images/posts/2025/08/pour-over.avif"
alt: "Сравнение Payload cms, Contentful cms, Strapi cms"
---
## Введение
Выбор Headless CMS — одно из самых важных архитектурных решений при создании сайта на Astro. Правильная CMS не только упрощает управление контентом, но и значительно влияет на опыт разработки и конечную производительность. На рынке доминируют такие гиганты, как Strapi и Contentful, но все большую популярность набирает **Payload CMS**, и на то есть веские причины.
В этой статье мы сравним эти три системы и разберемся, почему Payload CMS часто оказывается наиболее подходящим выбором для проектов на Astro.
## Критерий 1: Подход к конфигурации (Код против GUI)
Это фундаментальное различие между системами.
- **Payload CMS:** Придерживается подхода **config-as-code (конфигурация как код)**. Вся структура данных, поля, связи и даже права доступа описываются в TypeScript. Это означает, что вся конфигурация вашей CMS находится в Git, ее легко версионировать, переносить и воспроизводить. Для разработчика это огромный плюс.
- **Strapi:** Использует **GUI-first подход**. Вы создаете типы контента (Content Types) через веб-интерфейс. Это может быть удобно для быстрого старта, но создает проблемы с синхронизацией конфигурации между разными окружениями (dev, staging, production) и усложняет версионирование.
- **Contentful:** Работает по модели **API-first**, но конфигурация также происходит в веб-интерфейсе. Это удобно в рамках SaaS-платформы, но дает меньше контроля и прозрачности.
**Вердикт для Astro:** Экосистема Astro ориентирована на разработчиков, которые ценят контроль и работу с кодом. Подход Payload `config-as-code` идеально вписывается в эту философию, делая разработку предсказуемой и надежной.
## Критерий 2: Гибкость и расширяемость
- **Payload CMS:** Максимальная гибкость. Payload — это, по сути, надстройка над фреймворком Express.js. Вы можете легко добавить собственные API эндпоинты, написать сложную бизнес-логику или интегрироваться с любыми сторонними сервисами на уровне кода.
- **Strapi:** Хорошая расширяемость через систему плагинов и внутреннюю архитектуру. Однако для глубокой кастомизации часто требуется досконально изучить внутреннее API Strapi, что может быть сложнее, чем работа с Express в Payload.
- **Contentful:** Наименее гибкий вариант. Вы ограничены возможностями, которые предоставляет платформа. Расширение функциональности возможно через UI Extensions и внешние приложения, но это не дает такого же уровня контроля, как self-hosted решения.
**Вердикт для Astro:** Если вашему проекту может потребоваться нестандартная логика, Payload предоставляет практически безграничные возможности без "костылей".
## Критерий 3: Хостинг и стоимость
- **Payload CMS:** Полностью **self-hosted**. Вы можете разместить его где угодно: на Vercel, Render, AWS или собственном сервере. Вы платите только за ресурсы хостинга, а все функции Payload доступны бесплатно и без ограничений. Это самый экономичный вариант.
- **Strapi:** Также **self-hosted**, но предлагает и платный облачный сервис Strapi Cloud, где стоимость зависит от тарифного плана.
- **Contentful:** Это **SaaS (Software as a Service)**. Вы не заботитесь о хостинге, но платите за использование. Цены могут быстро вырасти при увеличении количества пользователей, записей или трафика, а многие продвинутые функции доступны только в дорогих тарифах.
**Вердикт для Astro:** Для большинства проектов на Astro (блоги, портфолио, корпоративные сайты) модель Payload является самой выгодной, так как позволяет избежать высоких ежемесячных платежей и не ограничивает функциональность.
## Сравнительная таблица
| Критерий | Payload CMS | Strapi | Contentful |
|:---|:---|:---|:---|
| **Конфигурация** | Код (TypeScript) | GUI | GUI |
| **Гибкость** | Очень высокая (Express.js) | Высокая (плагины) | Ограниченная |
| **Хостинг** | Self-hosted | Self-hosted / Платное облако | SaaS (платная подписка) |
| **Версионирование** | Идеально (Git) | Сложно | Через UI/API |
| **Лучше всего для** | Проектов, где важен контроль, гибкость и версионирование | Быстрого старта с GUI и готовых плагинов | Корпоративных клиентов, готовых платить за поддержку и SaaS |
## Заключение
Strapi и Contentful — это мощные и зрелые платформы, которые отлично подходят для многих задач. Однако для разработчика на Astro, который ценит скорость, контроль над кодом и экономическую эффективность, **Payload CMS часто оказывается лучшим выбором**.
Его `config-as-code` подход, непревзойденная гибкость и отсутствие скрытых платежей делают его идеальным партнером для создания молниеносных и легко поддерживаемых сайтов в экосистеме Astro.

View file

@ -0,0 +1,102 @@
---
title: "Создаем молниеносный блог с нуля на Astro и MDX"
description: "Пошаговое руководство по созданию высокопроизводительного блога с использованием Astro и MDX для быстрого рендеринга и удобного управления контентом."
publishDate: "2025-08-26"
author: "Sergey"
tags: ["Astro", "MDX", "Блог"]
coverImage:
src: "/images/posts/2025/08/astro-blog.avif"
alt: "Иллюстрация блога на Astro"
---
## Введение
В современном мире скорость загрузки сайта и удобство управления контентом играют ключевую роль. Astro, с его уникальной архитектурой "островов", и MDX, позволяющий писать JSX прямо в Markdown, представляют собой идеальное сочетание для создания молниеносных и функциональных блогов. В этом руководстве мы пройдем путь от установки Astro до публикации вашего первого поста, используя все преимущества этих технологий.
## 1. Настройка проекта Astro
Начнем с инициализации нового проекта Astro. Убедитесь, что у вас установлен Node.js (рекомендуется LTS-версия).
Для создания нового проекта Astro выполните следующую команду в терминале:
```bash
pnpm create astro@latest
```
Следуйте инструкциям в консоли. Выберите шаблон "Blog" для быстрого старта. После установки перейдите в директорию проекта:
```bash
cd my-astro-blog
pnpm install
```
Теперь вы можете запустить локальный сервер разработки:
```bash
pnpm run dev
```
Ваш блог будет доступен по адресу `http://localhost:4321/`.
## 2. Интеграция MDX для динамичного контента
MDX позволяет вам писать компоненты JSX прямо внутри ваших Markdown-файлов, что делает контент невероятно гибким и интерактивным. Для начала установите интеграцию MDX в ваш проект Astro:
```bash
pnpm astro add mdx
```
После установки Astro автоматически обновит ваш `astro.config.mjs`.
Теперь вы можете создавать файлы с расширением `.mdx` в директории `src/content/blog/` (или любой другой, которую вы настроили для постов). Внутри MDX-файла вы можете использовать как обычный Markdown, так и импортировать и рендерить компоненты React, Vue, Svelte и т.д.
Пример MDX-файла (`src/content/blog/my-first-mdx-post.mdx`):
```mdx
---
title: "Мой первый MDX пост"
publishDate: "2025-08-25"
description: "Пример использования MDX в Astro."
---
import MyComponent from "../../components/MyComponent.astro";
# Привет, MDX!
Это обычный Markdown-текст.
<MyComponent message="Я интерактивный компонент!" />
Вы можете встраивать любые компоненты прямо в ваш контент.
```
## 3. Создание компонентов и страниц
Ваши посты будут автоматически обрабатываться Astro. Вам нужно лишь создать соответствующие макеты и компоненты для отображения контента.
Например, вы можете создать компонент `src/components/MyComponent.astro`:
```astro
---
const { message } = Astro.props;
---
<div>
<p>{message}</p>
</div>
```
Убедитесь, что ваш макет поста (например, `src/layouts/LayoutPost.astro`) использует `<slot />` для рендеринга содержимого MDX-файла.
## 4. Оптимизация и развертывание
Одним из главных преимуществ Astro является его производительность. Он генерирует статический HTML, что делает его идеальным для развертывания на CDN. Для сборки вашего блога выполните:
```bash
pnpm run build
```
Результаты сборки будут находиться в директории `dist/`. Вы можете развернуть эту директорию на любом статическом хостинге, таком как Netlify, Vercel, Cloudflare Pages или GitHub Pages. Благодаря минимальному количеству JavaScript, ваш блог будет загружаться практически мгновенно, обеспечивая отличный пользовательский опыт и высокие показатели SEO.
## Заключение
Создание блога с Astro и MDX — это мощный и эффективный способ получить высокопроизводительный, SEO-оптимизированный и легко управляемый контент. Сочетание серверного рендеринга Astro и гибкости MDX позволяет создавать интерактивные и динамичные посты без ущерба для скорости. Начните свой проект сегодня и убедитесь в преимуществах этой современной связки технологий.

View file

@ -0,0 +1,69 @@
---
title: "Что такое Astro? Ключевые преимущества фреймворка в {year} году"
description: "Подробный разбор веб-фреймворка Astro. Узнайте, как его архитектура Zero-JS-by-default и Islands Architecture обеспечивают высочайшую производительность."
publishDate: "2025-08-24"
author: "Sergey"
tags: ["Astro", "Фреймворк", "JavaScript"]
coverImage:
src: "/images/posts/2025/08/astro.avif"
alt: "Абстрактный код на холсте"
---
## Введение
В мире веб-разработки, где каждую неделю появляются новые инструменты, бывает сложно уследить за трендами. Однако некоторые фреймворки выделяются на общем фоне, предлагая уникальный подход к созданию сайтов. Один из таких — **Astro**. Это современный фреймворк, созданный для построения быстрых, контент-ориентированных веб-сайтов.
## Что такое Astro?
Astro — это веб-фреймворк, предназначенный для создания многостраничных приложений (MPA) с упором на максимальную производительность. В отличие от SPA-фреймворков (Single Page Application), таких как React или Vue, которые загружают весь сайт как одно большое JavaScript-приложение, Astro генерирует статичные HTML-страницы во время сборки. Это означает, что пользователь получает готовый контент практически мгновенно.
Идеальные проекты для Astro — это блоги, портфолио, маркетинговые сайты, лендинги и сайты электронной коммерции. То есть любые проекты, где скорость загрузки и SEO играют ключевую роль.
## Концепция №1: Zero JS by Default
Главный принцип Astro — **ноль клиентского JavaScript по умолчанию**. Что это значит? Astro обрабатывает весь ваш код на сервере во время сборки и отдает в браузер только чистый HTML и CSS. JavaScript, который вы пишете (например, для компонента React), не будет отправлен пользователю, если вы этого не укажете явно.
Это радикально снижает объем загружаемых данных и время на их обработку в браузере, что напрямую влияет на показатели Core Web Vitals и общее впечатление пользователя.
## Концепция №2: Islands Architecture (Архитектура Островов)
Но что делать, если на странице нужна интерактивность? Например, кнопка "добавить в корзину", слайдер или форма поиска. Для этого Astro использует **Архитектуру Островов**.
Представьте вашу статическую HTML-страницу как океан. "Острова" — это отдельные интерактивные компоненты, которые "плавают" в этом океане. Каждый такой остров загружает свой собственный, изолированный JavaScript, не затрагивая остальную часть страницы.
С помощью специальных директив вы можете контролировать, как и когда загружается JavaScript для каждого компонента:
- `client:load`: Загрузить JS сразу же.
- `client:idle`: Загрузить JS, когда основной поток браузера освободится.
- `client:visible`: Загрузить JS, когда компонент появится в области видимости пользователя.
Этот подход позволяет добавлять интерактивность точечно, сохраняя при этом высочайшую производительность.
## Ключевые преимущества Astro
### 1. Непревзойденная производительность
За счет минимального использования JavaScript на клиенте сайты на Astro загружаются молниеносно. Это дает огромное преимущество в пользовательском опыте и SEO.
### 2. Гибкость в выборе UI-фреймворка
Вы можете использовать компоненты, написанные на **React, Preact, Svelte, Vue, SolidJS** и других фреймворках, в одном и том же проекте. Astro возьмет на себя их рендеринг на сервере, позволяя вам использовать любимые инструменты без ущерба для производительности.
### 3. Ориентация на контент
В Astro встроена первоклассная поддержка Markdown и MDX. Система "коллекций контента" (Content Collections) позволяет типизировать и валидировать ваши markdown-файлы, что делает управление большим блогом или сайтом с документацией простым и надежным.
### 4. Превосходное SEO
Так как Astro по умолчанию генерирует статический HTML, поисковым роботам очень легко сканировать и индексировать ваш сайт. Это обеспечивает отличные позиции в поисковой выдаче "из коробки".
## Astro или Next.js?
Next.js — это мощный фреймворк, но он ориентирован на создание сложных веб-приложений (SPA). По умолчанию он загружает значительно больше клиентского JavaScript.
- **Выбирайте Astro**, если ваш приоритет — скорость и контент: блоги, портфолио, новостные сайты, документация.
- **Выбирайте Next.js**, если вы создаете сложное интерактивное приложение, похожее на десктопное: социальные сети, дашборды, сложные редакторы.
## Заключение
Astro — это не просто еще один фреймворк. Это сдвиг парадигмы в сторону производительности и эффективности. Он берет лучшие идеи из мира статических сайтов и сложных JavaScript-приложений, объединяя их в мощный и удобный инструмент. Если вы хотите создавать самые быстрые сайты в вебе, Astro — ваш выбор №1 в 2025 году.

View file

@ -0,0 +1,116 @@
---
title: "Почему SolidJS — это будущее: Сравнение с React"
description: "Сравнение SolidJS и React. Узнайте, как отказ от Virtual DOM в SolidJS обеспечивает лучшую производительность и более интуитивный опыт разработки."
publishDate: "2025-09-09"
author: "Сергей"
tags: ["SolidJS", "React", "Производительность"]
coverImage:
src: "/images/posts/2025/09/solidjs-vs-react.avif"
alt: "Логотипы SolidJS и React"
---
## Введение
React десятилетиями был королем фронтенд-разработки. Его декларативный подход и компонентная модель изменили то, как мы строим пользовательские интерфейсы. Но что, если я скажу вам, что существует инструмент, который предлагает похожий опыт разработки, но с радикально лучшей производительностью? Встречайте **SolidJS**.
Этот фреймворк быстро набирает популярность, и многие называют его будущим веба. В этой статье мы разберемся, что делает SolidJS таким особенным, сравним его напрямую с React и на примерах кода покажем, почему за ним стоит следить.
## Что такое SolidJS?
SolidJS — это декларативная JavaScript-библиотека для создания пользовательских интерфейсов. Звучит знакомо? Да, на первый взгляд, он очень похож на React. Вы пишете компоненты в виде функций, используете JSX и управляете состоянием.
Однако ключевое отличие кроется "под капотом". SolidJS — это **по-настоящему реактивная** библиотека, которая **не использует Virtual DOM (VDOM)**.
## Главное отличие: Реактивность без Virtual DOM
Чтобы понять преимущество SolidJS, нужно вспомнить, как работает React.
### Как работает React?
Когда состояние в React-компоненте меняется (например, через `useState`), React заново выполняет всю функцию компонента. Затем он создает новое "дерево" объектов в памяти (Virtual DOM), сравнивает его со старой версией, находит различия (этот процесс называется "diffing" или "reconciliation") и только потом применяет эти изменения к реальному DOM.
Этот подход работает, но он избыточен. Повторный вызов целых функций и сравнение деревьев — это дополнительная работа, которая потребляет ресурсы.
### Как работает SolidJS?
SolidJS избавляется от этого посредника. Вместо VDOM он использует тонко настроенную систему реактивности, основанную на "сигналах" (Signals).
1. **Компиляция:** Во время сборки SolidJS компилирует ваш JSX в очень эффективный нативный JavaScript-код, который создает DOM-узлы напрямую.
2. **Реактивность:** Компоненты SolidJS выполняются **только один раз**. Когда вы создаете "сигнал" (аналог `useState`), SolidJS отслеживает, где именно в DOM используется его значение.
3. **Точечные обновления:** При изменении сигнала SolidJS не перезапускает компонент. Он обновляет **только тот конкретный текстовый узел или атрибут** в DOM, который зависел от этого сигнала.
Это как снайперский выстрел вместо ковровой бомбардировки.
## Сравнение на практике: Компонент счетчика
Давайте посмотрим на код. Вот простой счетчик в React и SolidJS.
### Счетчик на React
```jsx
import { useState, useEffect } from 'react';
function ReactCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Компонент ReactCounter был перерисован");
});
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>React Cчетчик: {count}</p>
<button onClick={increment}>Прибавить</button>
</div>
);
}
```
При каждом клике на кнопку вы увидите в консоли сообщение "Компонент ReactCounter был перерисован". Вся функция выполняется заново.
### Счетчик на SolidJS
```jsx
import { createSignal, onCleanup } from 'solid-js';
function SolidCounter() {
const [count, setCount] = createSignal(0);
console.log("Компонент SolidCounter выполнился. Этот лог появится только один раз!");
const increment = () => {
setCount(count() + 1);
};
// onCleanup для демонстрации, что компонент не пересоздается
onCleanup(() => {
console.log("Компонент SolidCounter удален");
});
return (
<div>
<p>SolidJS Cчетчик: {count()}</p>
<button onClick={increment}>Прибавить</button>
</div>
);
}
```
Здесь сообщение в консоли появится **только при первом рендере**. При последующих кликах SolidJS будет обновлять только текст внутри тега `<p>`, не затрагивая ничего другого. Функция компонента больше никогда не будет вызвана.
## Ключевые преимущества SolidJS
1. **Невероятная производительность:** Отсутствие VDOM и точечные обновления делают SolidJS одним из самых быстрых UI-фреймворков в мире, опережая React, Vue и Svelte в большинстве бенчмарков.
2. **Интуитивный код:** API очень похож на React Hooks, что делает переход легким. Однако правила проще: не нужно беспокоиться о массивах зависимостей в `useEffect` (`createEffect` в Solid) или использовать `useCallback`.
3. **Меньший размер бандла:** Код, генерируемый SolidJS, очень компактный. Ваше приложение будет весить меньше и загружаться быстрее.
4. **Настоящая декларативность:** Вы описываете, *как* состояние связано с UI, и SolidJS берет на себя всю остальную магию, создавая эффективный и оптимизированный граф зависимостей.
## Заключение
React — все еще фантастический и надежный инструмент. Но если вы ищете максимальную производительность, меньший размер и более элегантную модель реактивности без лишних абстракций, **SolidJS — это то, на что стоит обратить внимание**.
Он берет лучшие идеи React, но реализует их на более низком уровне, ближе к "железу" браузера. Это не просто "еще один фреймворк", а потенциальный взгляд на будущее веб-разработки — будущее без Virtual DOM.

View file

@ -0,0 +1,120 @@
---
title: "WordPress — это прошлое. 10 причин, почему Payload CMS — это будущее управления контентом."
description: "WordPress уходит в прошлое. Узнайте 10 причин, почему разработчики выбирают Headless CMS, такие как Payload, для скорости, безопасности и гибкости."
publishDate: "2025-09-05"
author: "Sergey"
tags: ["Payload CMS", "WordPress", "Headless CMS"]
coverImage:
src: "/images/posts/2025/09/pour-over.avif"
alt: "Сравнение WordPress и Payload CMS"
---
## Введение
На протяжении почти двух десятилетий WordPress был синонимом слова "веб-сайт". Он демократизировал публикацию контента, позволив миллионам людей без технических знаний создавать блоги и сайты. Но веб изменился. Сегодняшние требования к производительности, безопасности, гибкости и опыту разработчика (DX) заставляют нас переосмыслить инструменты, которые мы используем.
Монолитная архитектура WordPress, основанная на PHP и плагинах, все чаще становится узким местом. На смену ей приходит новое поколение Headless CMS, и **Payload CMS** — один из самых ярких его представителей. Это не просто "еще одна CMS", это принципиально иной подход. Вот 10 причин, почему Payload — это будущее, а WordPress рискует остаться в прошлом.
## 1. Архитектура Headless-First: Свобода без границ
<div class="bg-neutral-800/50 p-4 rounded-lg my-4">
**WordPress:** Это монолит. Он жестко связывает админ-панель (backend) с системой шаблонов (frontend). Хотите использовать современный frontend-фреймворк, такой как React, Vue или Svelte? Это возможно, но через "костыли" в виде REST API, которое не было изначально спроектировано для таких задач.
**Payload CMS:** Это Headless по своей природе. Он предоставляет вам мощную админ-панель и гибкий API, а что вы будете делать с этим API — ваше дело. Вы можете создавать нативные мобильные приложения, сверхбыстрые статические сайты на Astro или Next.js, или даже отправлять контент на умные часы. Вы получаете полную свободу выбора технологий.
</div>
## 2. Code-First, а не Click-First: Конфигурация как код
<div class="bg-neutral-800/50 p-4 rounded-lg my-4">
**WordPress:** Создание кастомных типов постов и полей (Custom Fields) — это царство плагинов вроде ACF и бесконечных кликов в админ-панели. Эту конфигурацию сложно версионировать, переносить между окружениями и поддерживать в команде.
**Payload CMS:** Вы определяете структуру вашего контента (коллекции) прямо в коде, используя TypeScript. Это дает невероятные преимущества: автодополнение, строгую типизацию, возможность хранить всю конфигурацию в Git, а также легкое развертывание и командную работу. Ваша схема данных — это часть вашего приложения.
</div>
## 3. Современный стек: TypeScript, Node.js, React
<div class="bg-neutral-800/50 p-4 rounded-lg my-4">
**WordPress:** Основан на PHP. Это надежный язык, но его синтаксис и экосистема для многих современных разработчиков выглядят устаревшими. Админ-панель написана с использованием jQuery и Backbone.js.
**Payload CMS:** Построен на технологиях, которые сегодня являются стандартом индустрии. Node.js и Express на бэкенде, React и TypeScript в админ-панели. Это означает, что ваш проект легко поддерживать, а найти для него разработчиков — гораздо проще.
</div>
## 4. Никакого "Ада плагинов"
<div class="bg-neutral-800/50 p-4 rounded-lg my-4">
**WordPress:** Хотите SEO? Плагин. Нужен кэш? Плагин. Двухфакторная аутентификация? Тоже плагин. Сайт на 20-30 плагинов от разных авторов — обычное дело. Это создает огромные дыры в безопасности, проблемы с производительностью и кошмар при обновлениях.
**Payload CMS:** Базовый функционал, такой как аутентификация, права доступа, загрузка файлов и предпросмотр, встроен в ядро. Функционал расширяется через чистую и понятную систему плагинов, которые являются просто функциями, а не черными ящиками.
</div>
## 5. Полный контроль и собственный хостинг
<div class="bg-neutral-800/50 p-4 rounded-lg my-4">
**WordPress:** Часто привязывает вас к специфическому хостингу, оптимизированному под PHP и MySQL.
**Payload CMS:** Это просто Node.js приложение. Вы можете захостить его где угодно: на Vercel, AWS, DigitalOcean или на собственном сервере. Вы полностью контролируете свою инфраструктуру и данные.
</div>
## 6. Феноменальный Developer Experience (DX)
<div class="bg-neutral-800/50 p-4 rounded-lg my-4">
**Payload CMS:** Создан разработчиками для разработчиков. Отличная документация, предсказуемый API, локальная разработка, которая доставляет удовольствие. Все направлено на то, чтобы вы могли творить, а не бороться с системой.
**WordPress:** Разработка под WordPress часто включает в себя изучение сотен хуков, функций с неочевидными названиями и "The Loop". Это может быть очень медленным и разочаровывающим процессом.
</div>
## 7. Производительность по умолчанию
<div class="bg-neutral-800/50 p-4 rounded-lg my-4">
**WordPress:** Каждый плагин и сложная тема добавляют нагрузку. Без агрессивного кэширования добиться высокой производительности на WordPress-сайте очень сложно.
**Payload CMS:** Так как он отдает только чистые данные через API, ваш frontend может быть максимально оптимизирован. Связка Payload + статический генератор сайтов (Astro, Next.js) по умолчанию создает одни из самых быстрых сайтов в мире.
</div>
## 8. Масштабируемость
<div class="bg-neutral-800/50 p-4 rounded-lg my-4">
**WordPress:** Может справляться с высокой нагрузкой, но это требует серьезных инженерных усилий и затрат на инфраструктуру.
**Payload CMS:** Построенный на Node.js и MongoDB/PostgreSQL, он изначально спроектирован для асинхронных операций и отлично масштабируется горизонтально, следуя современным облачным практикам.
</div>
## 9. Безопасность
<div class="bg-neutral-800/50 p-4 rounded-lg my-4">
**WordPress:** Является целью №1 для хакеров во всем мире из-за своей популярности и огромного количества уязвимых плагинов.
**Payload CMS:** Имеет гораздо меньшую площадь атаки. Современные методы аутентификации (HTTP-only cookies, JWT), гранулярная система прав доступа и отсутствие зависимости от сторонних плагинов делают его на порядок более безопасным решением.
</div>
## 10. Это не просто CMS, это фреймворк
<div class="bg-neutral-800/50 p-4 rounded-lg my-4">
**WordPress:** Это CMS, которую пытаются превратить во фреймворк для создания приложений.
**Payload CMS:** Это фреймворк для создания приложений, который "из коробки" дает вам мощную CMS. Вы можете построить на его основе не только сайт, но и сложную CRM, backend для SaaS-платформы или любую другую систему, где требуется управление контентом и данными.
</div>
## Заключение
WordPress проделал огромную работу и навсегда останется в истории веба. Но технологии не стоят на месте. Для современных, быстрых, безопасных и масштабируемых проектов нужны современные инструменты. Payload CMS предлагает именно такой инструмент — с фокусом на разработчика, гибкостью и производительностью, которые необходимы вебу будущего.

3
frontend/src/env.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
/// <reference path="./globalInterfaces.ts" />

View file

@ -0,0 +1,55 @@
// Типы для контактной информации
export type ContactInfo =
| {
readonly label: string;
readonly icon: string;
readonly value: string;
readonly isHtml?: false;
readonly link?: string;
readonly subValue?: string;
}
| {
readonly label: string;
readonly icon: string;
readonly value: string;
readonly isHtml: true;
}
| {
readonly label: string;
readonly icon: string;
readonly emails: readonly string[];
};
// Тип для полей формы
export interface FormField {
label: string;
placeholder: string;
type: string;
name: string;
}
// Тип для элементов формы
export type FormRow = FormField[];
// Тип для тегов блога
export interface BlogTag {
id: string;
name: string;
description?: string;
}
// Тип для постов блога
export interface BlogPost {
title: string;
description: string;
pubDate: Date;
author: string;
image?: string;
tags: string[];
draft?: boolean;
id?: string;
collectionId?: string;
}
// Тип для таймера
export type TimeoutId = number | null;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols Light by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M5.616 21q-.691 0-1.153-.462T4 19.385V6.615q0-.69.463-1.152T5.616 5h1.769V3.308q0-.233.153-.386t.385-.153t.386.153t.153.386V5h7.154V3.27q0-.214.143-.358t.357-.143t.356.143t.144.357V5h1.769q.69 0 1.153.463T20 6.616v12.769q0 .69-.462 1.153T18.384 21zm0-1h12.769q.23 0 .423-.192t.192-.424v-8.768H5v8.769q0 .23.192.423t.423.192M12 14.154q-.31 0-.54-.23t-.23-.54t.23-.539t.54-.23t.54.23t.23.54t-.23.539t-.54.23m-4 0q-.31 0-.54-.23t-.23-.54t.23-.539t.54-.23t.54.23t.23.54t-.23.539t-.54.23m8 0q-.31 0-.54-.23t-.23-.54t.23-.539t.54-.23t.54.23t.23.54t-.23.539t-.54.23M12 18q-.31 0-.54-.23t-.23-.54t.23-.539t.54-.23t.54.23t.23.54t-.23.54T12 18m-4 0q-.31 0-.54-.23t-.23-.54t.23-.539t.54-.23t.54.23t.23.54t-.23.54T8 18m8 0q-.31 0-.54-.23t-.23-.54t.23-.539t.54-.23t.54.23t.23.54t-.23.54T16 18"/></svg>

After

Width:  |  Height:  |  Size: 1,021 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Myna UI Icons by Praveen Juge - https://github.com/praveenjuge/mynaui-icons/blob/main/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m15 6l-6 6l6 6"/></svg>

After

Width:  |  Height:  |  Size: 327 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zm8-7l8-5V6l-8 5l-8-5v2z"/></svg>

After

Width:  |  Height:  |  Size: 364 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M22 12c0-5.52-4.48-10-10-10S2 6.48 2 12c0 4.84 3.44 8.87 8 9.8V15H8v-3h2V9.5C10 7.57 11.57 6 13.5 6H16v3h-2c-.55 0-1 .45-1 1v2h3v3h-3v6.95c5.05-.5 9-4.76 9-9.95"/></svg>

After

Width:  |  Height:  |  Size: 419 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93zM6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37z"/></svg>

After

Width:  |  Height:  |  Size: 593 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Design Icons by Pictogrammers - https://github.com/Templarian/MaterialDesign/blob/master/LICENSE --><path fill="currentColor" d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.2 4.2 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.52 8.52 0 0 1-5.33 1.84q-.51 0-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23"/></svg>

After

Width:  |  Height:  |  Size: 719 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols Light by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 11.385q-1.237 0-2.119-.882T9 8.385t.881-2.12T12 5.386t2.119.88t.881 2.12t-.881 2.118t-2.119.882m-7 7.23V16.97q0-.619.36-1.158q.361-.54.97-.838q1.416-.679 2.834-1.018q1.417-.34 2.836-.34t2.837.34t2.832 1.018q.61.298.97.838q.361.539.361 1.158v1.646z"/></svg>

After

Width:  |  Height:  |  Size: 493 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Google Material Icons by Material Design Authors - https://github.com/material-icons/material-icons/blob/master/LICENSE --><path fill="currentColor" d="M19.05 4.91A9.82 9.82 0 0 0 12.04 2c-5.46 0-9.91 4.45-9.91 9.91c0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38c1.45.79 3.08 1.21 4.74 1.21c5.46 0 9.91-4.45 9.91-9.91c0-2.65-1.03-5.14-2.9-7.01m-7.01 15.24c-1.48 0-2.93-.4-4.2-1.15l-.3-.18l-3.12.82l.83-3.04l-.2-.31a8.26 8.26 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.24-8.24c2.2 0 4.27.86 5.82 2.42a8.18 8.18 0 0 1 2.41 5.83c.02 4.54-3.68 8.23-8.22 8.23m4.52-6.16c-.25-.12-1.47-.72-1.69-.81c-.23-.08-.39-.12-.56.12c-.17.25-.64.81-.78.97c-.14.17-.29.19-.54.06c-.25-.12-1.05-.39-1.99-1.23c-.74-.66-1.23-1.47-1.38-1.72c-.14-.25-.02-.38.11-.51c.11-.11.25-.29.37-.43s.17-.25.25-.41c.08-.17.04-.31-.02-.43s-.56-1.34-.76-1.84c-.2-.48-.41-.42-.56-.43h-.48c-.17 0-.43.06-.66.31c-.22.25-.86.85-.86 2.07s.89 2.4 1.01 2.56c.12.17 1.75 2.67 4.23 3.74c.59.26 1.05.41 1.41.52c.59.19 1.13.16 1.56.1c.48-.07 1.47-.6 1.67-1.18c.21-.58.21-1.07.14-1.18s-.22-.16-.47-.28"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,48 @@
---
import '@assets/css/global.css'
import Header from '@components/layout/header/Header.astro'
import Footer from '@components/layout/footer/Footer.astro'
import Breadcrumbs, { Breadcrumb } from '@components/base/Breadcrumbs.astro'
interface Props {
title: string;
description?: string;
canonicalURL?: string;
breadcrumbs?: Breadcrumb[];
hideNav?: boolean;
}
const { title, description, canonicalURL: propCanonicalURL, breadcrumbs, hideNav = false } = Astro.props
const canonicalURL: string = propCanonicalURL ? propCanonicalURL : Astro.url.pathname
---
<!doctype html>
<html lang="ru" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Noto+Sans:wght@400;500;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
</head>
<body class="antialiased bg-background-light dark:bg-background-dark text-[#181611] dark:text-white font-body flex flex-col min-h-screen overflow-x-hidden">
{!hideNav && <Header />}
{!hideNav && breadcrumbs && breadcrumbs.length > 0 && <Breadcrumbs breadcrumbs={breadcrumbs} />}
<main class="flex-grow w-full relative flex flex-col">
<slot />
</main>
<Footer />
</body>
</html>

View file

@ -0,0 +1,10 @@
import PocketBase from 'pocketbase';
export const pb = new PocketBase(import.meta.env.PUBLIC_POCKETBASE_URL);
// Это отключает авто-отмену глобально для всех запросов
pb.autoCancellation = false;
export function getImageUrl(record, filename) {
return `${import.meta.env.PUBLIC_POCKETBASE_URL}/api/files/${record.collectionId}/${record.id}/${filename}`;
}

View file

@ -0,0 +1,30 @@
/**
* Функция для обработки HTML-контента
* 1. Удаляет пустые заголовки (которые создают лишние цифры в нумерации)
* 2. Добавляет атрибут alt к изображениям, если его нет
*/
export function processHtmlContent(htmlContent: string): string {
if (!htmlContent) return '';
let processed = htmlContent;
// --- ШАГ 1: Удаляем пустые заголовки ---
// Проблема: Редактор создает <h2>&nbsp;</h2> при нажатии Enter. CSS считает это за пункт и ставит цифру.
// Решение: Удаляем все h1-h6, внутри которых только пробелы, &nbsp; или <br>
processed = processed.replace(/<h[1-6][^>]*>(?:\s|&nbsp;|<br\/?>)*<\/h[1-6]>/gi, '');
// --- ШАГ 2 (Опционально): Удаляем ручную нумерацию из текста ---
// Если у вас в CSS настроена авто-нумерация, а в тексте вы пишете "3. Заголовок",
// то на сайте будет "3. 3. Заголовок".
// Раскомментируйте строку ниже, чтобы убрать цифры из текста и оставить только CSS:
// processed = processed.replace(/(<h[2-6][^>]*>)\s*\d+(\.\d+)*\.?\s*/gi, '$1');
// --- ШАГ 3: Обработка изображений (ваш старый код) ---
// Ищем теги <img> без атрибута alt и добавляем пустой alt
const imgWithoutAltRegex = /<img(?![^>]*\balt\b)([^>]*)>/gi;
processed = processed.replace(imgWithoutAltRegex, '<img$1 alt="">');
return processed;
}

View file

@ -0,0 +1,105 @@
---
import Layout from '@layouts/Layout.astro';
import Button from '@components/base/Button.astro';
import { SITE } from '@constants/site';
const title = `404 - Маршрут не найден | ${SITE.TITLE}`;
const description = "Запрашиваемая страница не существует. Маршрут не построен.";
const breadcrumbs = [
{ label: 'Главная', url: '/' },
{ label: 'Ошибка 404' }
];
---
<Layout
title={title}
description={description}
canonicalURL={SITE.PAGES.ERROR_404}
breadcrumbs={breadcrumbs}
hideNav={true}
>
<!-- Секция 404: занимает всю высоту экрана, так как Header и Footer скрыты -->
<section class="relative min-h-screen flex items-center justify-center overflow-hidden bg-[#181611] py-20 px-4">
<!-- 1. ФОНОВЫЙ ДЕКОР: Индустриальные диагональные полосы (как на знаках опасности) -->
<div
class="absolute inset-0 opacity-[0.03] pointer-events-none"
style="background-image: repeating-linear-gradient(45deg, #f2a60d, #f2a60d 2px, transparent 2px, transparent 30px);"
></div>
<!-- 2. ГИГАНТСКИЕ ЦИФРЫ НА ЗАДНЕМ ПЛАНЕ -->
<div class="absolute inset-0 flex items-center justify-center opacity-[0.02] select-none pointer-events-none">
<span class="text-[25rem] md:text-[40rem] font-black font-display leading-none">404</span>
</div>
<!-- 3. ОСНОВНОЙ КОНТЕНТ -->
<div class="max-w-3xl w-full text-center z-10">
<!-- Иконка "Маршрут не найден" с анимацией пульсации -->
<div class="inline-flex items-center justify-center size-24 bg-[#f2a60d]/10 border border-[#f2a60d]/30 rounded mb-10 relative">
<span class="material-symbols-outlined text-[#f2a60d] text-6xl animate-pulse">
wrong_location
</span>
<!-- Индикатор "сигнала" -->
<span class="absolute -top-1 -right-1 flex h-4 w-4">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f2a60d] opacity-75"></span>
<span class="relative inline-flex rounded-full h-4 w-4 bg-[#f2a60d]"></span>
</span>
</div>
<!-- Текстовый блок -->
<div class="flex flex-col gap-4">
<h1 class="text-white text-5xl md:text-8xl font-black font-display uppercase tracking-tighter leading-none">
Маршрут <br/>
<span class="text-[#f2a60d]">не построен</span>
</h1>
<!-- Оранжевый разделитель -->
<div class="h-1.5 w-24 bg-[#f2a60d] mx-auto my-6"></div>
<p class="text-gray-400 text-lg md:text-xl font-light max-w-lg mx-auto leading-relaxed font-body">
Похоже, ваш запрос отклонился от заданного курса. <br class="hidden md:block"/>
Груз не найден, или страница была перемещена.
</p>
</div>
<!-- КНОПКИ (используем ваш универсальный компонент Button) -->
<div class="flex flex-col sm:flex-row items-center justify-center gap-5 mt-12">
<Button
href="/"
variant="primary"
className="w-full sm:w-auto shadow-[0_0_20px_rgba(242,166,13,0.3)]"
>
Вернуться на базу
</Button>
<Button
href="/contacts"
variant="outline"
className="w-full sm:w-auto border-white/20 hover:border-[#f2a60d] hover:text-[#f2a60d]"
>
Связаться с диспетчером
</Button>
</div>
<!-- НИЖНЯЯ ТЕХНИЧЕСКАЯ МАРКИРОВКА -->
<div class="mt-20 flex items-center justify-center gap-4 text-gray-700 font-display text-[10px] uppercase tracking-[0.4em]">
<span class="w-16 h-[1px] bg-gray-800"></span>
SYSTEM ERROR: 404_ROUTE_LOST
<span class="w-16 h-[1px] bg-gray-800"></span>
</div>
</div>
</section>
</Layout>
<style>
/* Анимация плавного появления всей страницы */
section {
animation: pageReveal 1s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes pageReveal {
from { opacity: 0; transform: scale(0.98); }
to { opacity: 1; transform: scale(1); }
}
</style>

View file

@ -0,0 +1,20 @@
---
import Layout from '@layouts/Layout.astro'
import AboutHero from '@components/about/AboutHero.astro'
import Contacts from '@components/about/ContactCTA.astro'
import { SITE } from '@constants/site'
const title = `Обо мне - ${SITE.TITLE}`
const description = SITE.PAGE_DESCRIPTIONS.ABOUT
const breadcrumbs = [
{ label: 'Главная', url: '/' },
{ label: 'Обо мне' }
];
---
<Layout title={title} description={description} canonicalURL={SITE.PAGES.ABOUT} breadcrumbs={breadcrumbs}>
<main class="max-w-3xl mx-auto my-12 px-4 sm:px-6 lg:px-0 space-y-16">
<AboutHero />
<Contacts />
</main>
</Layout>

View file

@ -0,0 +1,21 @@
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
export const GET: APIRoute = async (): Promise<Response> => {
const allPosts: CollectionEntry<"post">[] = await getCollection("post");
const searchData = allPosts.map((post) => ({
slug: post.slug,
title: post.data.title,
description: post.data.description,
body: post.body,
}));
return new Response(JSON.stringify(searchData), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
};

View file

@ -0,0 +1,3 @@
---
---

View file

@ -0,0 +1,31 @@
---
import Layout from '@layouts/Layout.astro';
import BlogHero from '@components/base/Hero.astro';
import BlogFilters from '@components/blog/Filters.astro';
import BlogGrid from '@components/blog/BlogGrid.astro';
import Pagination from '@components/blog/Pagination.astro';
import CTA from '@components/blog/CTA.astro';
import { SITE } from '@constants/site';
const title = `Блог - ${SITE.TITLE}`;
const description = SITE.PAGE_DESCRIPTIONS.BLOG;
const breadcrumbs = [
{ label: 'Главная', url: '/' },
{ label: 'Блог' }
];
---
<Layout title={title} description={description} canonicalURL={SITE.PAGES.BLOG} breadcrumbs={breadcrumbs}>
<BlogHero
title="Блог"
subtitle="Экспертные статьи, кейсы перевозки негабаритных грузов и новости логистики. Читайте о том, как мы решаем сложные задачи."
categoryName="База знаний"
categoryIcon="local_shipping"
placeholder="Поиск по статьям..."
gradientWord="компании"
/>
<BlogFilters />
<BlogGrid />
<Pagination />
<CTA />
</Layout>

View file

@ -0,0 +1,3 @@
---
---

View file

@ -0,0 +1,22 @@
---
import Layout from '@layouts/Layout.astro';
import Hero from '@components/cars/Hero.astro';
import StatsSection from '@components/cars/StatsSection.astro';
import TrucksGrid from '@components/cars/TrucksGrid.astro';
import CTA from '@components/cars/CTA.astro';
import { SITE } from '@constants/site';
const title = `Автопарк - ${SITE.TITLE}`;
const description = SITE.PAGE_DESCRIPTIONS.CARS;
const breadcrumbs = [
{ label: 'Главная', url: '/' },
{ label: 'Автопарк' }
];
---
<Layout title={title} description={description} canonicalURL={SITE.PAGES.CARS} breadcrumbs={breadcrumbs}>
<Hero />
<StatsSection />
<TrucksGrid />
<CTA />
</Layout>

View file

@ -0,0 +1,49 @@
---
import Layout from '@layouts/Layout.astro';
import ContactHero from '@components/contacts/ContactHero.astro';
import ContactInfo from '@components/contacts/ContactInfo.astro';
import ContactMap from '@components/contacts/ContactMap.astro';
import ContactForm from '@components/contacts/ContactForm.astro';
import { SITE } from '@constants/site';
const title = `Контакты | ${SITE.TITLE}`;
const description = SITE.PAGE_DESCRIPTIONS.CONTACT;
const canonicalURL = SITE.PAGES.CONTACT;
const breadcrumbs = [
{ label: 'Главная', url: '/' },
{ label: 'Контакты' }
];
---
<Layout title={title} description={description} canonicalURL={canonicalURL} breadcrumbs={breadcrumbs}>
<ContactHero />
<div class="flex-grow max-w-[1440px] mx-auto w-full px-6 lg:px-12 py-12 lg:py-20">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20">
<!-- Левая колонка: Инфо и Карта -->
<div class="lg:col-span-5 flex flex-col gap-12">
<ContactInfo />
<ContactMap />
</div>
<!-- Правая колонка: Форма -->
<div class="lg:col-span-7">
<ContactForm />
</div>
</div>
</div>
<!-- Декоративная полоса перед футером -->
<div class="border-t border-white/10 bg-background-dark py-6 px-6 lg:px-12">
<div class="max-w-[1440px] mx-auto flex flex-col md:flex-row justify-between items-center gap-4 text-xs text-gray-600 uppercase tracking-widest font-mono">
<span>© {new Date().getFullYear()} {SITE.TITLE}</span>
<span class="hidden md:block">/// PROJECT.CARGO.LOGISTICS</span>
<div class="flex gap-6">
<a class="hover:text-primary transition-colors" href="#">Политика</a>
<a class="hover:text-primary transition-colors" href="#">Реквизиты</a>
</div>
</div>
</div>
</Layout>

View file

@ -0,0 +1,19 @@
---
import Layout from '@layouts/Layout.astro';
import Hero from '@components/home/Hero.astro';
import Services from '@components/home/Services.astro';
import Projects from '@components/home/Projects.astro';
import CallToAction from '@components/home/CallToAction.astro';
import { SITE } from '@constants/site';
const title = `${SITE.TITLE} - Негабаритные грузоперевозки`;
const description = SITE.DESCRIPTION;
const canonicalURL = SITE.URL;
---
<Layout title={title} description={description} canonicalURL={canonicalURL}>
<Hero />
<Services />
<Projects />
<CallToAction />
</Layout>

View file

@ -0,0 +1,3 @@
---
---

View file

@ -0,0 +1,32 @@
---
import Layout from '@layouts/Layout.astro';
import Hero from '@components/base/Hero.astro';
import FeaturedNews from '@components/news/FeaturedNews.astro';
import NewsFilters from '@components/news/Filters.astro';
import NewsGrid from '@components/news/NewsGrid.astro';
import Pagination from '@components/news/Pagination.astro';
import { SITE } from '@constants/site';
const title = `Новости компании | ${SITE.TITLE}`;
const description = SITE.PAGE_DESCRIPTIONS.NEWS;
const breadcrumbs = [
{ label: 'Главная', url: '/' },
{ label: 'Новости' }
];
---
<Layout title={title} description={description} canonicalURL={SITE.PAGES.NEWS} breadcrumbs={breadcrumbs}>
<Hero
title="Новости"
subtitle="Последние обновления, события и объявления от ХимТрансСервис"
categoryName="Корпоративные новости"
categoryIcon="campaign"
placeholder="Поиск по новостям..."
gradientWord="компании"
/>
<FeaturedNews />
<NewsFilters />
<NewsGrid />
<Pagination />
</Layout>

View file

@ -0,0 +1,3 @@
---
---

View file

@ -0,0 +1,22 @@
---
import Layout from '@layouts/Layout.astro';
import Hero from '@components/projects/Hero.astro';
import Filters from '@components/projects/Filters.astro';
import ProjectsGrid from '@components/projects/ProjectsGrid.astro';
import CTA from '@components/projects/CTA.astro';
import { SITE } from '@constants/site';
const title = `Проекты - ${SITE.TITLE}`;
const description = SITE.PAGE_DESCRIPTIONS.PROJECTS;
const breadcrumbs = [
{ label: 'Главная', url: '/' },
{ label: 'Проекты' }
];
---
<Layout title={title} description={description} canonicalURL={SITE.PAGES.PROJECTS} breadcrumbs={breadcrumbs}>
<Hero />
<Filters />
<ProjectsGrid />
<CTA />
</Layout>

View file

@ -0,0 +1,23 @@
---
import Layout from '@layouts/Layout.astro';
import ServicesHero from '@components/services/ServicesHero.astro';
import ServicesGrid from '@components/services/ServicesGrid.astro';
import Stats from '@components/services/Stats.astro';
import CTA from '@components/home/CallToAction.astro';
import { SITE } from '@constants/site';
const title = `Наши услуги | ${SITE.TITLE}`;
const description = SITE.PAGE_DESCRIPTIONS.SERVICES;
const canonicalURL = SITE.PAGES.SERVICES;
const breadcrumbs = [
{ label: 'Главная', url: '/' },
{ label: 'Услуги' }
];
---
<Layout title={title} description={description} canonicalURL={canonicalURL} breadcrumbs={breadcrumbs}>
<ServicesHero />
<ServicesGrid />
<Stats />
<CTA />
</Layout>

View file

@ -0,0 +1,3 @@
import { atom } from "nanostores";
export const isSearchOpen = atom(false);

Some files were not shown because too many files have changed in this diff Show more