first commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# Скопируйте этот файл в .env.local и заполните реальными значениями.
|
||||
# .env.local никогда не должен попадать в git (уже добавлен в .gitignore).
|
||||
|
||||
# Токен вашего Telegram-бота, полученный у @BotFather
|
||||
TELEGRAM_BOT_TOKEN=your_new_bot_token_from_botfather
|
||||
|
||||
# ID чата/канала, куда бот должен присылать заявки с сайта.
|
||||
# Узнать свой chat_id можно, написав боту @userinfobot,
|
||||
# либо получив его через метод getUpdates вашего бота.
|
||||
TELEGRAM_CHAT_ID=991309145
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<list>
|
||||
<option value="aiogram" />
|
||||
<option value="python-dotenv" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.14 (TG_BOT)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (TG_BOT)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/labnethub.iml" filepath="$PROJECT_DIR$/.idea/labnethub.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# Запуск сайта LabNetHub на macOS
|
||||
|
||||
## 1. Проверьте, установлен ли Node.js
|
||||
|
||||
Откройте Terminal (⌘+Space → введите «Terminal» → Enter) и выполните:
|
||||
|
||||
```bash
|
||||
node -v
|
||||
npm -v
|
||||
```
|
||||
|
||||
- Если видите номер версии Node **18.17 и выше** (например, `v20.x`) — переходите к шагу 3.
|
||||
- Если команда не найдена или версия ниже — переходите к шагу 2.
|
||||
|
||||
## 2. Установите Node.js
|
||||
|
||||
**Вариант А — официальный установщик (самый простой):**
|
||||
|
||||
1. Откройте https://nodejs.org
|
||||
2. Скачайте версию **LTS**
|
||||
3. Запустите скачанный `.pkg`-файл и пройдите установку по шагам
|
||||
|
||||
**Вариант Б — через Homebrew** (если уже пользуетесь brew):
|
||||
|
||||
```bash
|
||||
brew install node@20
|
||||
brew link node@20
|
||||
```
|
||||
|
||||
После установки закройте и снова откройте Terminal, затем проверьте:
|
||||
|
||||
```bash
|
||||
node -v
|
||||
npm -v
|
||||
```
|
||||
|
||||
## 3. Распакуйте архив проекта
|
||||
|
||||
Дважды кликните `labnethub-website.zip` в Finder — рядом появится папка `labnethub`.
|
||||
|
||||
Либо через терминал:
|
||||
|
||||
```bash
|
||||
cd ~/Downloads
|
||||
unzip labnethub-website.zip
|
||||
cd labnethub
|
||||
```
|
||||
|
||||
## 4. Установите зависимости проекта
|
||||
|
||||
Находясь внутри папки `labnethub`, выполните:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Это скачает Next.js, React, Tailwind и остальные пакеты. При первом запуске занимает пару минут.
|
||||
|
||||
## 5. Настройте переменные окружения
|
||||
|
||||
```bash
|
||||
cp .env.local.example .env.local
|
||||
open -e .env.local
|
||||
```
|
||||
|
||||
В открывшемся файле впишите свои значения:
|
||||
|
||||
```
|
||||
TELEGRAM_BOT_TOKEN=токен_от_BotFather
|
||||
TELEGRAM_CHAT_ID=ваш_chat_id
|
||||
```
|
||||
|
||||
Сохраните (⌘+S) и закройте окно.
|
||||
|
||||
## 6. Запустите сайт локально
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
В терминале появится адрес — откройте в браузере:
|
||||
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
Чтобы остановить сервер — нажмите `Control + C` в терминале.
|
||||
|
||||
## 7. Проверка продакшен-сборки (по желанию)
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
|
||||
Так сайт запустится в том же режиме, в котором будет работать на реальном сервере.
|
||||
|
||||
## Частые проблемы
|
||||
|
||||
| Проблема | Решение |
|
||||
|---|---|
|
||||
| `command not found: npm` | Node не установлен, либо терминал открыт до установки — закройте и откройте Terminal заново |
|
||||
| Порт 3000 занят | Next.js сам предложит другой порт, либо укажите вручную: `npm run dev -- -p 3001` |
|
||||
| Ошибки при `npm install` | Проверьте версию Node (`node -v`) — нужна 18.17 или выше |
|
||||
| `EACCES` / ошибки прав доступа | Не используйте `sudo npm install` — переустановите Node через официальный установщик или Homebrew |
|
||||
|
||||
## Что дальше
|
||||
|
||||
Инструкции по заполнению реальных контактов, деплою на сервер и настройке безопасности — в `README.md` в корне проекта.
|
||||
@@ -0,0 +1,101 @@
|
||||
# LabNetHub — сайт компании
|
||||
|
||||
Сайт на Next.js 14 (App Router) + TypeScript + Tailwind CSS для компании
|
||||
LabNetHub: разработка сайтов, ботов для соцсетей, настройка CRM, хостинг
|
||||
и IT-поддержка.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp .env.local.example .env.local # заполните реальными значениями
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Сайт откроется на http://localhost:3000
|
||||
|
||||
Сборка для продакшена:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
app/ — страницы (App Router)
|
||||
page.tsx — Главная
|
||||
websites/ — Сайты
|
||||
bots/ — Боты
|
||||
crm/ — CRM
|
||||
hosting/ — Хостинг и серверы
|
||||
support/ — IT-поддержка
|
||||
contacts/ — Контакты
|
||||
api/contact/ — API-роут формы обратной связи (→ Telegram)
|
||||
components/ — переиспользуемые компоненты (Header, Footer, карточки и т.д.)
|
||||
config/
|
||||
contacts.ts — все контакты и ссылки на соцсети в одном месте
|
||||
services.ts — данные для карточек услуг и навигации
|
||||
```
|
||||
|
||||
## Что нужно заполнить перед публикацией
|
||||
|
||||
1. **Контакты** — откройте `config/contacts.ts` и замените placeholder-ссылки
|
||||
на реальные: Telegram, VK, WhatsApp, email, город.
|
||||
|
||||
2. **Telegram-уведомления формы** — в `.env.local` укажите:
|
||||
- `TELEGRAM_BOT_TOKEN` — токен бота, полученный у [@BotFather](https://t.me/BotFather)
|
||||
- `TELEGRAM_CHAT_ID` — ID чата/канала для приёма заявок.
|
||||
Узнать свой ID можно, написав боту [@userinfobot](https://t.me/userinfobot),
|
||||
либо через метод `getUpdates` вашего бота.
|
||||
|
||||
3. **Домен** — в `app/layout.tsx` поле `metadataBase` укажите на ваш реальный домен.
|
||||
|
||||
## Форма обратной связи
|
||||
|
||||
Форма на странице `/contacts` отправляет данные на `POST /api/contact`.
|
||||
Роут проверяет обязательные поля, убирает HTML-теги (защита от инъекций),
|
||||
экранирует спецсимволы Markdown и пересылает сообщение в Telegram через
|
||||
Bot API. В форме также есть скрытое honeypot-поле для защиты от простых ботов.
|
||||
|
||||
## Безопасность
|
||||
|
||||
В `next.config.mjs` включены базовые защитные HTTP-заголовки:
|
||||
`Strict-Transport-Security`, `X-Frame-Options`, `X-Content-Type-Options`,
|
||||
`Referrer-Policy`, `Permissions-Policy`.
|
||||
|
||||
Рекомендации для сервера при деплое:
|
||||
|
||||
- Раздавайте сайт только по HTTPS (например, сертификат Let's Encrypt через certbot).
|
||||
- На уровне nginx настройте `limit_req` для `/api/contact`, чтобы ограничить
|
||||
количество запросов от одного адреса в минуту.
|
||||
- Храните `.env.local` вне репозитория — файл уже добавлен в `.gitignore`.
|
||||
- Регулярно обновляйте зависимости (`npm outdated` / `npm audit`).
|
||||
|
||||
## Деплой на свой сервер (LabNetHub VDS/VPS)
|
||||
|
||||
Пример запуска через PM2 за nginx как обратным прокси:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pm2 start npm --name labnethub -- start
|
||||
```
|
||||
|
||||
В конфиге nginx проксируйте запросы на `http://127.0.0.1:3000` и подключите
|
||||
SSL-сертификат. При желании сайт также можно собрать в статику
|
||||
(`output: "export"` в `next.config.mjs`) — тогда `/api/contact` работать не будет,
|
||||
и форму нужно будет переключить на внешний сервис отправки писем.
|
||||
|
||||
## Цветовая тема
|
||||
|
||||
Токены дизайна заданы в `tailwind.config.ts`:
|
||||
|
||||
- `base` — тёмно-синий фон (`base.950` — основной фон страницы)
|
||||
- `blue` — основной акцент бренда
|
||||
- `green` — акцент раздела «Хостинг и серверы»
|
||||
- `ink` — оттенки текста
|
||||
|
||||
Шрифты (`Unbounded`, `Manrope`, `JetBrains Mono`) подключены через
|
||||
`next/font/google` в `app/layout.tsx` — при сборке скачиваются один раз
|
||||
и дальше работают без обращений к Google Fonts во время выполнения.
|
||||
@@ -0,0 +1,98 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const MAX_LENGTHS = {
|
||||
name: 100,
|
||||
contact: 150,
|
||||
message: 2000,
|
||||
};
|
||||
|
||||
// Убираем HTML-теги и обрезаем длину, чтобы нельзя было сломать
|
||||
// форматирование сообщения в Telegram или протащить лишний код.
|
||||
function sanitize(value: unknown, maxLength: number): string {
|
||||
if (typeof value !== "string") return "";
|
||||
return value.replace(/<[^>]*>/g, "").trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
// Экранируем спецсимволы Telegram MarkdownV2.
|
||||
function escapeMarkdown(text: string): string {
|
||||
return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, (m) => `\\${m}`);
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||
const chatId = process.env.TELEGRAM_CHAT_ID;
|
||||
|
||||
if (!token || !chatId) {
|
||||
console.error("TELEGRAM_BOT_TOKEN или TELEGRAM_CHAT_ID не заданы");
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Сервис временно недоступен, попробуйте позже" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Некорректный запрос" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { name, contact, message } = (body ?? {}) as Record<string, unknown>;
|
||||
|
||||
const cleanName = sanitize(name, MAX_LENGTHS.name);
|
||||
const cleanContact = sanitize(contact, MAX_LENGTHS.contact);
|
||||
const cleanMessage = sanitize(message, MAX_LENGTHS.message);
|
||||
|
||||
if (!cleanName || !cleanContact || !cleanMessage) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Заполните имя, контакт и сообщение" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const text = [
|
||||
"*Новая заявка с сайта LabNetHub*",
|
||||
"",
|
||||
`*Имя:* ${escapeMarkdown(cleanName)}`,
|
||||
`*Контакт:* ${escapeMarkdown(cleanContact)}`,
|
||||
`*Сообщение:*`,
|
||||
escapeMarkdown(cleanMessage),
|
||||
].join("\n");
|
||||
|
||||
try {
|
||||
const telegramRes = await fetch(
|
||||
`https://api.telegram.org/bot${token}/sendMessage`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: "MarkdownV2",
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!telegramRes.ok) {
|
||||
const details = await telegramRes.text();
|
||||
console.error("Telegram API error:", details);
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Не удалось отправить сообщение" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error("Ошибка отправки в Telegram:", err);
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "Не удалось отправить сообщение" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Send,
|
||||
MessageCircle,
|
||||
ArrowUpRight,
|
||||
Check,
|
||||
Bell,
|
||||
ShoppingBag,
|
||||
Headphones,
|
||||
Workflow,
|
||||
Mail,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import SectionEyebrow from "@/components/SectionEyebrow";
|
||||
import CTASection from "@/components/CTASection";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Боты для Telegram и VK",
|
||||
description:
|
||||
"Разработка Telegram- и VK-ботов любой сложности: автоматизация заявок, уведомления, интеграция с CRM.",
|
||||
};
|
||||
|
||||
const platforms = [
|
||||
{
|
||||
icon: Send,
|
||||
title: "Telegram-боты",
|
||||
description:
|
||||
"Боты любой сложности: приём заявок, уведомления, интеграция с CRM и сервисами.",
|
||||
points: ["Диалоговые сценарии и кнопки", "Оплата и интеграции", "Уведомления в реальном времени"],
|
||||
},
|
||||
{
|
||||
icon: MessageCircle,
|
||||
title: "Боты для ВКонтакте",
|
||||
description:
|
||||
"Боты для сообществ VK: рассылки, магазины в чате, поддержка подписчиков.",
|
||||
points: ["Автоответы и сценарии диалога", "Магазин прямо в сообществе", "Рассылки по базе подписчиков"],
|
||||
},
|
||||
];
|
||||
|
||||
const useCases = [
|
||||
{
|
||||
icon: FileText,
|
||||
title: "Приём заявок",
|
||||
description: "Клиент оставляет заявку в чате — она сразу попадает в CRM или менеджеру.",
|
||||
},
|
||||
{
|
||||
icon: Bell,
|
||||
title: "Уведомления",
|
||||
description: "Автоматические напоминания клиентам и сотрудникам о статусе заказа.",
|
||||
},
|
||||
{
|
||||
icon: ShoppingBag,
|
||||
title: "Магазин в чате",
|
||||
description: "Каталог, корзина и оформление заказа без перехода на сайт.",
|
||||
},
|
||||
{
|
||||
icon: Headphones,
|
||||
title: "Поддержка клиентов",
|
||||
description: "Ответы на частые вопросы и передача сложных случаев менеджеру.",
|
||||
},
|
||||
{
|
||||
icon: Workflow,
|
||||
title: "Интеграция с CRM",
|
||||
description: "Бот пишет и читает данные напрямую из вашей CRM-системы.",
|
||||
},
|
||||
{
|
||||
icon: Mail,
|
||||
title: "Рассылки",
|
||||
description: "Массовые и сегментированные рассылки по базе клиентов.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function BotsPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
eyebrow="Боты для соцсетей"
|
||||
title="Боты, которые"
|
||||
highlight="работают без выходных"
|
||||
subtitle="Создаём Telegram- и VK-ботов любой сложности: автоматизация заявок, уведомления, интеграция с CRM и сервисами вашего бизнеса."
|
||||
>
|
||||
<Link
|
||||
href="/contacts"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-blue-500 px-6 py-3.5 font-medium text-white transition-transform hover:scale-[1.03]"
|
||||
>
|
||||
Обсудить бота
|
||||
<ArrowUpRight size={18} />
|
||||
</Link>
|
||||
</PageHero>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto grid max-w-6xl gap-5 lg:grid-cols-2">
|
||||
{platforms.map((platform) => (
|
||||
<div
|
||||
key={platform.title}
|
||||
className="rounded-2xl border border-white/[0.06] bg-base-900/50 p-8"
|
||||
>
|
||||
<div className="mb-5 flex h-11 w-11 items-center justify-center rounded-xl border border-blue-500/25 bg-blue-500/10 text-blue-400">
|
||||
<platform.icon size={20} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 className="font-display text-xl font-medium">{platform.title}</h3>
|
||||
<p className="mt-3 text-ink-500">{platform.description}</p>
|
||||
<ul className="mt-6 space-y-3">
|
||||
{platform.points.map((point) => (
|
||||
<li key={point} className="flex items-start gap-2.5 text-sm text-ink-300">
|
||||
<Check size={17} className="mt-0.5 shrink-0 text-blue-400" />
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<SectionEyebrow>Сценарии использования</SectionEyebrow>
|
||||
<h2 className="font-display text-3xl font-medium sm:text-4xl">
|
||||
Что умеют наши боты
|
||||
</h2>
|
||||
|
||||
<div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{useCases.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="rounded-2xl border border-white/[0.06] bg-base-900/40 p-6"
|
||||
>
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg border border-white/[0.08] bg-white/[0.03] text-blue-400">
|
||||
<item.icon size={18} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 className="font-medium text-ink-100">{item.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-ink-500">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTASection
|
||||
title="Готовы автоматизировать общение с клиентами?"
|
||||
subtitle="Опишите задачу — предложим сценарий бота и сроки разработки."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Send, MessageCircle, Phone, Mail, MapPin, Clock } from "lucide-react";
|
||||
import SectionEyebrow from "@/components/SectionEyebrow";
|
||||
import NetworkGraphic from "@/components/NetworkGraphic";
|
||||
import ContactForm from "@/components/ContactForm";
|
||||
import { contacts } from "@/config/contacts";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Контакты",
|
||||
description: "Свяжитесь с командой LabNetHub через форму, Telegram, WhatsApp или email.",
|
||||
};
|
||||
|
||||
const directContacts = [
|
||||
{
|
||||
icon: Send,
|
||||
label: "Telegram",
|
||||
value: "Написать в чат",
|
||||
href: contacts.telegramBot,
|
||||
},
|
||||
{
|
||||
icon: Phone,
|
||||
label: "WhatsApp",
|
||||
value: "Написать в WhatsApp",
|
||||
href: contacts.whatsapp,
|
||||
},
|
||||
{
|
||||
icon: Mail,
|
||||
label: "Email",
|
||||
value: contacts.email,
|
||||
href: `mailto:${contacts.email}`,
|
||||
},
|
||||
{
|
||||
icon: MessageCircle,
|
||||
label: "ВКонтакте",
|
||||
value: "Наше сообщество",
|
||||
href: contacts.vk,
|
||||
},
|
||||
];
|
||||
|
||||
export default function ContactsPage() {
|
||||
return (
|
||||
<>
|
||||
<section className="relative overflow-hidden border-b border-white/[0.06] px-6 pb-16 pt-32 sm:pb-20 sm:pt-40">
|
||||
<div className="pointer-events-none absolute right-0 top-0 w-[420px] max-w-[60%] opacity-70 sm:opacity-100">
|
||||
<NetworkGraphic className="w-full" />
|
||||
</div>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<SectionEyebrow>Контакты</SectionEyebrow>
|
||||
<h1 className="text-balance font-display text-4xl font-medium leading-[1.1] sm:text-5xl">
|
||||
Свяжитесь <span className="text-gradient-blue">с нами</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-xl text-lg leading-relaxed text-ink-500">
|
||||
Расскажите о задаче — предложим решение и сроки. Форма ниже
|
||||
приходит нам напрямую в Telegram.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto grid max-w-6xl gap-12 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<div>
|
||||
<h2 className="mb-6 font-display text-xl font-medium">
|
||||
Напишите нам
|
||||
</h2>
|
||||
<ContactForm />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="mb-6 font-display text-xl font-medium">
|
||||
Прямые контакты
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{directContacts.map((c) => (
|
||||
<a
|
||||
key={c.label}
|
||||
href={c.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-4 rounded-xl border border-white/[0.06] bg-base-900/50 p-4 transition-colors hover:border-blue-500/30"
|
||||
>
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-blue-500/25 bg-blue-500/10 text-blue-400">
|
||||
<c.icon size={18} strokeWidth={1.75} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-ink-500">{c.label}</p>
|
||||
<p className="font-medium text-ink-100">{c.value}</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 space-y-3 border-t border-white/[0.06] pt-6">
|
||||
<div className="flex items-center gap-3 text-sm text-ink-500">
|
||||
<MapPin size={16} className="text-ink-700" />
|
||||
{contacts.city}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-ink-500">
|
||||
<Clock size={16} className="text-ink-700" />
|
||||
{contacts.workingHours}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { ArrowUpRight, Check, Lock, Wallet, Settings2, Database } from "lucide-react";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import SectionEyebrow from "@/components/SectionEyebrow";
|
||||
import CTASection from "@/components/CTASection";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Настройка и интеграция CRM",
|
||||
description:
|
||||
"Установка, настройка и интеграция CRM-систем с открытым исходным кодом под процессы вашего бизнеса.",
|
||||
};
|
||||
|
||||
const benefits = [
|
||||
{
|
||||
icon: Wallet,
|
||||
title: "Без лицензионных платежей",
|
||||
description: "Открытый исходный код — платите за настройку, а не за подписку на каждого пользователя.",
|
||||
},
|
||||
{
|
||||
icon: Lock,
|
||||
title: "Данные остаются у вас",
|
||||
description: "CRM разворачивается на вашем сервере — полный контроль над данными клиентов.",
|
||||
},
|
||||
{
|
||||
icon: Settings2,
|
||||
title: "Гибкая настройка",
|
||||
description: "Дорабатываем систему под ваши процессы, а не подстраиваем бизнес под шаблон.",
|
||||
},
|
||||
{
|
||||
icon: Database,
|
||||
title: "Свобода интеграций",
|
||||
description: "Подключаем сайт, боты, почту и другие сервисы без ограничений вендора.",
|
||||
},
|
||||
];
|
||||
|
||||
const systems = [
|
||||
{
|
||||
name: "EspoCRM",
|
||||
description: "Гибкая CRM с удобным конструктором сущностей и процессов под любые отрасли.",
|
||||
},
|
||||
{
|
||||
name: "SuiteCRM",
|
||||
description: "Развитая система с модулями продаж, поддержки и маркетинга из коробки.",
|
||||
},
|
||||
{
|
||||
name: "Odoo CRM",
|
||||
description: "Часть экосистемы Odoo — легко расширяется складом, документами и бухгалтерией.",
|
||||
},
|
||||
];
|
||||
|
||||
const included = [
|
||||
"Установка и настройка CRM на сервере",
|
||||
"Настройка воронок продаж и полей под ваши процессы",
|
||||
"Интеграция с сайтом, ботами и почтой",
|
||||
"Перенос текущей базы клиентов",
|
||||
"Обучение сотрудников работе с системой",
|
||||
"Техническая поддержка после запуска",
|
||||
];
|
||||
|
||||
export default function CrmPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
eyebrow="CRM-системы"
|
||||
title="Настройка и"
|
||||
highlight="интеграция CRM"
|
||||
subtitle="Устанавливаем, настраиваем и интегрируем CRM с открытым исходным кодом — под процессы именно вашего бизнеса."
|
||||
>
|
||||
<Link
|
||||
href="/contacts"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-blue-500 px-6 py-3.5 font-medium text-white transition-transform hover:scale-[1.03]"
|
||||
>
|
||||
Обсудить внедрение
|
||||
<ArrowUpRight size={18} />
|
||||
</Link>
|
||||
</PageHero>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<SectionEyebrow>Почему open source</SectionEyebrow>
|
||||
<div className="mt-8 grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{benefits.map((b) => (
|
||||
<div key={b.title}>
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg border border-white/[0.08] bg-white/[0.03] text-blue-400">
|
||||
<b.icon size={18} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 className="font-medium text-ink-100">{b.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-ink-500">
|
||||
{b.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<SectionEyebrow>С какими системами работаем</SectionEyebrow>
|
||||
<h2 className="font-display text-3xl font-medium sm:text-4xl">
|
||||
Примеры CRM с открытым кодом
|
||||
</h2>
|
||||
<div className="mt-12 grid gap-5 lg:grid-cols-3">
|
||||
{systems.map((s) => (
|
||||
<div
|
||||
key={s.name}
|
||||
className="rounded-2xl border border-white/[0.06] bg-base-900/50 p-7"
|
||||
>
|
||||
<h3 className="font-display text-lg font-medium">{s.name}</h3>
|
||||
<p className="mt-3 text-sm leading-relaxed text-ink-500">
|
||||
{s.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-6 text-sm text-ink-700">
|
||||
Также работаем с другими open-source CRM — подберём систему под ваши задачи и бюджет.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl rounded-3xl border border-white/[0.06] bg-base-900/40 p-10">
|
||||
<SectionEyebrow>Что входит в услугу</SectionEyebrow>
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2">
|
||||
{included.map((item) => (
|
||||
<div key={item} className="flex items-start gap-2.5">
|
||||
<Check size={17} className="mt-0.5 shrink-0 text-blue-400" />
|
||||
<span className="text-ink-300">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTASection
|
||||
title="Хотите навести порядок в клиентской базе?"
|
||||
subtitle="Расскажем, какая CRM подойдёт именно вашим процессам."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-base-950 text-ink-100 font-body antialiased;
|
||||
background-image: theme("backgroundImage.grid-fade");
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
@apply font-display text-ink-100;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@apply bg-blue-600/40 text-ink-100;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid theme("colors.blue.400");
|
||||
outline-offset: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-base-950;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-base-700 rounded-full;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-blue-700;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-gradient-blue {
|
||||
@apply bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-blue-600;
|
||||
}
|
||||
.text-gradient-green {
|
||||
@apply bg-clip-text text-transparent bg-gradient-to-r from-green-300 to-green-500;
|
||||
}
|
||||
.clip-corner {
|
||||
clip-path: polygon(0 0, 100% 0, 100% calc(100% - 22px), calc(100% - 22px) 100%, 0 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Check,
|
||||
MapPin,
|
||||
Globe,
|
||||
Gauge,
|
||||
Settings2,
|
||||
Cpu,
|
||||
} from "lucide-react";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import SectionEyebrow from "@/components/SectionEyebrow";
|
||||
import CTASection from "@/components/CTASection";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Хостинг и серверы",
|
||||
description:
|
||||
"VDS/VPS на собственных серверах в Астрахани и у партнёров по всему миру. NVMe, SLA 99.9%, защита от DDoS.",
|
||||
};
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: MapPin,
|
||||
title: "Собственный дата-центр",
|
||||
description: "Современное оборудование в нашем дата-центре в Астрахани.",
|
||||
},
|
||||
{
|
||||
icon: Globe,
|
||||
title: "Локации по всему миру",
|
||||
description: "Европа, Азия, США и другие регионы у партнёров.",
|
||||
},
|
||||
{
|
||||
icon: Gauge,
|
||||
title: "Высокая производительность",
|
||||
description: "NVMe-диски и мощные процессоры для стабильной нагрузки.",
|
||||
},
|
||||
{
|
||||
icon: Settings2,
|
||||
title: "Гибкие конфигурации",
|
||||
description: "Соберём сервер под ваши задачи, а не по шаблону.",
|
||||
},
|
||||
];
|
||||
|
||||
const specs = [
|
||||
"От 1 до 32 vCPU",
|
||||
"От 1 до 128 GB RAM",
|
||||
"NVMe SSD",
|
||||
"Linux / Windows",
|
||||
"SLA 99.9%",
|
||||
"Защита от DDoS",
|
||||
"Резервное копирование",
|
||||
"Панель управления и API",
|
||||
];
|
||||
|
||||
export default function HostingPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
accent="green"
|
||||
eyebrow="Хостинг и серверы"
|
||||
title="Надёжные серверы"
|
||||
highlight="по всему миру"
|
||||
subtitle="Предоставляем мощные и стабильные VDS/VPS — на собственных серверах в Астрахани и в других локациях у проверенных партнёров."
|
||||
>
|
||||
<Link
|
||||
href="/contacts"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-green-500 px-6 py-3.5 font-medium text-base-950 transition-transform hover:scale-[1.03]"
|
||||
>
|
||||
Подобрать сервер
|
||||
<ArrowUpRight size={18} />
|
||||
</Link>
|
||||
</PageHero>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{features.map((f) => (
|
||||
<div key={f.title}>
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg border border-green-500/25 bg-green-500/10 text-green-400">
|
||||
<f.icon size={18} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 className="font-medium text-ink-100">{f.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-ink-500">
|
||||
{f.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto max-w-4xl rounded-3xl border border-green-500/20 bg-green-500/[0.04] p-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl border border-green-500/25 bg-green-500/10 text-green-400">
|
||||
<Cpu size={20} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h2 className="font-display text-xl font-medium">
|
||||
VDS/VPS от LabNetHub
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-8 grid gap-4 sm:grid-cols-2">
|
||||
{specs.map((spec) => (
|
||||
<div key={spec} className="flex items-start gap-2.5">
|
||||
<Check size={17} className="mt-0.5 shrink-0 text-green-400" />
|
||||
<span className="text-ink-300">{spec}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-8 text-sm text-ink-500">
|
||||
Стоимость зависит от конфигурации и локации — рассчитаем тариф под
|
||||
вашу задачу.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 pb-4">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<SectionEyebrow accent="green">Размещение проектов</SectionEyebrow>
|
||||
<p className="max-w-2xl text-ink-500">
|
||||
Размещаем сайты и боты, разработанные нашей командой, на
|
||||
собственных серверах или у партнёров — без необходимости искать
|
||||
хостинг отдельно и разбираться в настройке сервера самостоятельно.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTASection
|
||||
title="Нужен сервер под проект?"
|
||||
subtitle="Подберём конфигурацию и локацию, поможем с переносом и настройкой."
|
||||
accent="green"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#6FA8FF" />
|
||||
<stop offset="100%" stop-color="#2F63EB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="40" height="40" rx="9" fill="#0A1428" />
|
||||
<polygon points="10,7 18,7 15,32 7,32" fill="#F5F7FB" />
|
||||
<polygon points="14,32 33,32 28,24 18,24" fill="url(#g)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 440 B |
@@ -0,0 +1,59 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Unbounded, Manrope, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
const unbounded = Unbounded({
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["400", "500", "600"],
|
||||
variable: "--font-unbounded",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const manrope = Manrope({
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
variable: "--font-manrope",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin", "cyrillic"],
|
||||
weight: ["400", "500"],
|
||||
variable: "--font-jetbrains",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://labnethub.ru"),
|
||||
title: {
|
||||
default: "LabNetHub — комплексные IT-решения для бизнеса",
|
||||
template: "%s — LabNetHub",
|
||||
},
|
||||
description:
|
||||
"Сайты, боты для соцсетей, настройка CRM, хостинг и IT-поддержка. От идеи до стабильной работы вашей инфраструктуры.",
|
||||
openGraph: {
|
||||
title: "LabNetHub — комплексные IT-решения для бизнеса",
|
||||
description:
|
||||
"Сайты, боты для соцсетей, настройка CRM, хостинг и IT-поддержка.",
|
||||
locale: "ru_RU",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="ru" className={`${unbounded.variable} ${manrope.variable} ${jetbrainsMono.variable}`}>
|
||||
<body>
|
||||
<Header />
|
||||
<main className="min-h-screen pt-[73px]">{children}</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowUpRight, ShieldCheck, Rocket, SlidersHorizontal, Headphones } from "lucide-react";
|
||||
import SectionEyebrow from "@/components/SectionEyebrow";
|
||||
import ServiceCard from "@/components/ServiceCard";
|
||||
import FeatureBadge from "@/components/FeatureBadge";
|
||||
import NetworkGraphic from "@/components/NetworkGraphic";
|
||||
import CTASection from "@/components/CTASection";
|
||||
import { services } from "@/config/services";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<section className="relative overflow-hidden px-6 pb-20 pt-32 sm:pb-28 sm:pt-40">
|
||||
<div className="pointer-events-none absolute right-[-40px] top-10 w-[480px] max-w-[65%] opacity-60 sm:opacity-100">
|
||||
<NetworkGraphic accent="blue" className="w-full" />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<SectionEyebrow>Технологии · Решения · Поддержка</SectionEyebrow>
|
||||
<h1 className="text-balance font-display text-4xl font-medium leading-[1.08] sm:text-6xl">
|
||||
Комплексные IT-решения{" "}
|
||||
<span className="text-gradient-blue">для вашего бизнеса</span>
|
||||
</h1>
|
||||
<p className="mt-6 max-w-xl text-lg leading-relaxed text-ink-500">
|
||||
От идеи до стабильной работы инфраструктуры: сайты, боты для
|
||||
соцсетей, CRM, серверы и техническая поддержка — в одной команде.
|
||||
</p>
|
||||
|
||||
<div className="mt-9 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/contacts"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-blue-500 px-6 py-3.5 font-medium text-white transition-transform hover:scale-[1.03]"
|
||||
>
|
||||
Обсудить проект
|
||||
<ArrowUpRight size={18} />
|
||||
</Link>
|
||||
<Link
|
||||
href="#services"
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-white/[0.12] px-6 py-3.5 font-medium text-ink-100 transition-colors hover:bg-white/[0.05]"
|
||||
>
|
||||
Смотреть услуги
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-16 grid max-w-3xl grid-cols-2 gap-6 border-t border-white/[0.06] pt-8 sm:grid-cols-4">
|
||||
{[
|
||||
["5", "направлений услуг"],
|
||||
["24/7", "техподдержка"],
|
||||
["99.9%", "SLA на серверах"],
|
||||
["1", "команда на весь проект"],
|
||||
].map(([value, label]) => (
|
||||
<div key={label}>
|
||||
<p className="font-display text-2xl text-ink-100">{value}</p>
|
||||
<p className="mt-1 text-sm text-ink-500">{label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="services" className="scroll-mt-24 px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="max-w-2xl">
|
||||
<SectionEyebrow>Что мы делаем</SectionEyebrow>
|
||||
<h2 className="font-display text-3xl font-medium sm:text-4xl">
|
||||
Всё для роста вашего бизнеса — в одном месте
|
||||
</h2>
|
||||
<p className="mt-4 text-ink-500">
|
||||
Каждое направление можно заказать отдельно или как часть общего
|
||||
решения — от первого сайта до полноценной IT-инфраструктуры.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{services.map((service) => (
|
||||
<ServiceCard key={service.key} service={service} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl rounded-3xl border border-white/[0.06] bg-base-900/40 p-10">
|
||||
<SectionEyebrow>Почему выбирают нас</SectionEyebrow>
|
||||
<div className="mt-6 grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<FeatureBadge
|
||||
icon={ShieldCheck}
|
||||
title="Надёжность"
|
||||
description="Гарантия качества и стабильной работы решений"
|
||||
/>
|
||||
<FeatureBadge
|
||||
icon={Rocket}
|
||||
title="Скорость"
|
||||
description="Быстрое выполнение задач и запуск проектов"
|
||||
/>
|
||||
<FeatureBadge
|
||||
icon={SlidersHorizontal}
|
||||
title="Индивидуальный подход"
|
||||
description="Решения под конкретные задачи вашего бизнеса"
|
||||
/>
|
||||
<FeatureBadge
|
||||
icon={Headphones}
|
||||
title="Поддержка 24/7"
|
||||
description="Мы всегда на связи и готовы помочь"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTASection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { ArrowUpRight, Server, Activity, Wrench, MessageSquare, Check } from "lucide-react";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import SectionEyebrow from "@/components/SectionEyebrow";
|
||||
import CTASection from "@/components/CTASection";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "IT-поддержка и инфраструктура",
|
||||
description:
|
||||
"Обслуживание IT-инфраструктуры бизнеса: сборка серверов, мониторинг, устранение сбоев, консультации.",
|
||||
};
|
||||
|
||||
const coverage = [
|
||||
{
|
||||
icon: Server,
|
||||
title: "Обслуживание серверов",
|
||||
description: "Настройка, обновление и сопровождение серверного оборудования.",
|
||||
},
|
||||
{
|
||||
icon: Activity,
|
||||
title: "Мониторинг и профилактика",
|
||||
description: "Отслеживаем состояние систем, чтобы предупредить сбои заранее.",
|
||||
},
|
||||
{
|
||||
icon: Wrench,
|
||||
title: "Сборка и настройка оборудования",
|
||||
description: "Подбираем и настраиваем технику под задачи вашей команды.",
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "Консультации",
|
||||
description: "Помогаем принимать решения по развитию IT-инфраструктуры.",
|
||||
},
|
||||
];
|
||||
|
||||
const formats = [
|
||||
{
|
||||
title: "Разовая задача",
|
||||
description:
|
||||
"Точечная помощь: настройка сервера, устранение конкретной проблемы, консультация.",
|
||||
points: ["Без долгосрочных обязательств", "Оценка сроков после диагностики задачи"],
|
||||
},
|
||||
{
|
||||
title: "Постоянное обслуживание",
|
||||
description:
|
||||
"Регулярное сопровождение инфраструктуры: мониторинг, обновления, приоритетная поддержка.",
|
||||
points: ["Согласованный формат и время реакции", "Профилактика вместо экстренных починок"],
|
||||
},
|
||||
];
|
||||
|
||||
export default function SupportPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
eyebrow="IT-поддержка"
|
||||
title="Поддержка вашей"
|
||||
highlight="IT-инфраструктуры"
|
||||
subtitle="Обслуживаем существующую инфраструктуру бизнеса: от сборки и настройки оборудования до мониторинга и устранения сбоев."
|
||||
>
|
||||
<Link
|
||||
href="/contacts"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-blue-500 px-6 py-3.5 font-medium text-white transition-transform hover:scale-[1.03]"
|
||||
>
|
||||
Обсудить задачу
|
||||
<ArrowUpRight size={18} />
|
||||
</Link>
|
||||
</PageHero>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{coverage.map((c) => (
|
||||
<div key={c.title}>
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg border border-white/[0.08] bg-white/[0.03] text-blue-400">
|
||||
<c.icon size={18} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 className="font-medium text-ink-100">{c.title}</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-ink-500">
|
||||
{c.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<SectionEyebrow>Формат работы</SectionEyebrow>
|
||||
<h2 className="font-display text-3xl font-medium sm:text-4xl">
|
||||
Как удобно вам
|
||||
</h2>
|
||||
<div className="mt-12 grid gap-5 lg:grid-cols-2">
|
||||
{formats.map((f) => (
|
||||
<div
|
||||
key={f.title}
|
||||
className="rounded-2xl border border-white/[0.06] bg-base-900/50 p-8"
|
||||
>
|
||||
<h3 className="font-display text-lg font-medium">{f.title}</h3>
|
||||
<p className="mt-3 text-ink-500">{f.description}</p>
|
||||
<ul className="mt-6 space-y-3">
|
||||
{f.points.map((point) => (
|
||||
<li key={point} className="flex items-start gap-2.5 text-sm text-ink-300">
|
||||
<Check size={17} className="mt-0.5 shrink-0 text-blue-400" />
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTASection
|
||||
title="Нужна помощь с инфраструктурой?"
|
||||
subtitle="Опишите ситуацию — предложим формат работы и следующий шаг."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Check, ArrowUpRight } from "lucide-react";
|
||||
import PageHero from "@/components/PageHero";
|
||||
import SectionEyebrow from "@/components/SectionEyebrow";
|
||||
import CTASection from "@/components/CTASection";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Сайты для бизнеса",
|
||||
description:
|
||||
"Разработка сайтов-визиток и полноценных корпоративных сайтов для малого и среднего бизнеса.",
|
||||
};
|
||||
|
||||
const offers = [
|
||||
{
|
||||
title: "Сайт-визитка",
|
||||
description:
|
||||
"Компактный сайт на 1–5 страниц: рассказать о компании, показать услуги и получить первые заявки.",
|
||||
points: [
|
||||
"Адаптивная вёрстка под любые экраны",
|
||||
"Форма заявки и контакты на видном месте",
|
||||
"Быстрый запуск — от нескольких дней",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Корпоративный сайт под ключ",
|
||||
description:
|
||||
"Полноценный сайт для компании: каталог, блог, личный кабинет, интеграции с CRM и сервисами.",
|
||||
points: [
|
||||
"Структура под задачи бизнеса, а не шаблон",
|
||||
"SEO-оптимизация и подготовка к продвижению",
|
||||
"Интеграция с CRM, ботами и аналитикой",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const steps = [
|
||||
{
|
||||
n: "01",
|
||||
title: "Бриф",
|
||||
description: "Обсуждаем задачи бизнеса, аудиторию и нужный результат.",
|
||||
},
|
||||
{
|
||||
n: "02",
|
||||
title: "Прототип и дизайн",
|
||||
description: "Собираем структуру страниц и визуальный стиль сайта.",
|
||||
},
|
||||
{
|
||||
n: "03",
|
||||
title: "Разработка",
|
||||
description: "Вёрстка и программирование с адаптацией под все устройства.",
|
||||
},
|
||||
{
|
||||
n: "04",
|
||||
title: "Тестирование",
|
||||
description: "Проверяем работу форм, скорость и корректность на разных устройствах.",
|
||||
},
|
||||
{
|
||||
n: "05",
|
||||
title: "Запуск и поддержка",
|
||||
description: "Публикуем сайт и остаёмся на связи для доработок.",
|
||||
},
|
||||
];
|
||||
|
||||
const included = [
|
||||
"Адаптивная вёрстка под мобильные, планшеты и десктоп",
|
||||
"SEO-оптимизация: метатеги, семантическая разметка, скорость загрузки",
|
||||
"Интеграция форм обратной связи с Telegram, email или CRM",
|
||||
"Подключение аналитики (Яндекс.Метрика, Google Analytics)",
|
||||
"Настройка домена, SSL-сертификата и хостинга",
|
||||
"Инструкция по управлению сайтом после запуска",
|
||||
];
|
||||
|
||||
export default function WebsitesPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
eyebrow="Разработка сайтов"
|
||||
title="Сайты, которые"
|
||||
highlight="работают на бизнес"
|
||||
subtitle="Разрабатываем современные и адаптивные сайты для вашего бизнеса — от простой визитки до полноценного корпоративного портала."
|
||||
>
|
||||
<Link
|
||||
href="/contacts"
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-blue-500 px-6 py-3.5 font-medium text-white transition-transform hover:scale-[1.03]"
|
||||
>
|
||||
Обсудить проект
|
||||
<ArrowUpRight size={18} />
|
||||
</Link>
|
||||
<Link
|
||||
href="#process"
|
||||
className="inline-flex items-center gap-1.5 rounded-full border border-white/[0.12] px-6 py-3.5 font-medium text-ink-100 transition-colors hover:bg-white/[0.05]"
|
||||
>
|
||||
Как мы работаем
|
||||
</Link>
|
||||
</PageHero>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto grid max-w-6xl gap-5 lg:grid-cols-2">
|
||||
{offers.map((offer) => (
|
||||
<div
|
||||
key={offer.title}
|
||||
className="rounded-2xl border border-white/[0.06] bg-base-900/50 p-8"
|
||||
>
|
||||
<h3 className="font-display text-xl font-medium">{offer.title}</h3>
|
||||
<p className="mt-3 text-ink-500">{offer.description}</p>
|
||||
<ul className="mt-6 space-y-3">
|
||||
{offer.points.map((point) => (
|
||||
<li key={point} className="flex items-start gap-2.5 text-sm text-ink-300">
|
||||
<Check size={17} className="mt-0.5 shrink-0 text-blue-400" />
|
||||
{point}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="process" className="scroll-mt-24 px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<SectionEyebrow>Как мы работаем</SectionEyebrow>
|
||||
<h2 className="font-display text-3xl font-medium sm:text-4xl">
|
||||
Пять шагов от идеи до запуска
|
||||
</h2>
|
||||
|
||||
<div className="mt-12 grid gap-8 sm:grid-cols-2 lg:grid-cols-5">
|
||||
{steps.map((step) => (
|
||||
<div key={step.n}>
|
||||
<p className="font-mono text-sm text-blue-400">{step.n}</p>
|
||||
<h3 className="mt-2 font-display text-lg font-medium">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm leading-relaxed text-ink-500">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="px-6 py-20">
|
||||
<div className="mx-auto max-w-6xl rounded-3xl border border-white/[0.06] bg-base-900/40 p-10">
|
||||
<SectionEyebrow>Что входит в разработку</SectionEyebrow>
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2">
|
||||
{included.map((item) => (
|
||||
<div key={item} className="flex items-start gap-2.5">
|
||||
<Check size={17} className="mt-0.5 shrink-0 text-blue-400" />
|
||||
<span className="text-ink-300">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTASection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
interface CTASectionProps {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
accent?: "blue" | "green";
|
||||
}
|
||||
|
||||
export default function CTASection({
|
||||
title = "Готовы обсудить проект?",
|
||||
subtitle = "Расскажите, что нужно — предложим решение и сроки в течение рабочего дня.",
|
||||
accent = "blue",
|
||||
}: CTASectionProps) {
|
||||
const isGreen = accent === "green";
|
||||
return (
|
||||
<section className="px-6 py-20">
|
||||
<div
|
||||
className={`mx-auto flex max-w-4xl flex-col items-start justify-between gap-6 rounded-3xl border p-10 sm:flex-row sm:items-center ${
|
||||
isGreen
|
||||
? "border-green-500/20 bg-green-500/[0.04]"
|
||||
: "border-blue-500/20 bg-blue-500/[0.04]"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<h2 className="font-display text-2xl font-medium text-ink-100">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="mt-2 max-w-md text-ink-500">{subtitle}</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/contacts"
|
||||
className={`inline-flex shrink-0 items-center gap-1.5 rounded-full px-6 py-3 font-medium text-base-950 transition-transform hover:scale-[1.03] ${
|
||||
isGreen ? "bg-green-400" : "bg-blue-400"
|
||||
}`}
|
||||
>
|
||||
Связаться с нами
|
||||
<ArrowUpRight size={18} />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent } from "react";
|
||||
import { Loader2, CheckCircle2, AlertCircle } from "lucide-react";
|
||||
|
||||
type Status = "idle" | "loading" | "success" | "error";
|
||||
|
||||
export default function ContactForm() {
|
||||
const [status, setStatus] = useState<Status>("idle");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setStatus("loading");
|
||||
setErrorMessage("");
|
||||
|
||||
const form = e.currentTarget;
|
||||
const data = new FormData(form);
|
||||
|
||||
// Honeypot: если поле заполнено — это бот, тихо считаем успехом.
|
||||
if (data.get("website")) {
|
||||
setStatus("success");
|
||||
form.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: String(data.get("name") || ""),
|
||||
contact: String(data.get("contact") || ""),
|
||||
message: String(data.get("message") || ""),
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/contact", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok || !result.ok) {
|
||||
throw new Error(result.error || "Не удалось отправить сообщение");
|
||||
}
|
||||
|
||||
setStatus("success");
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
setStatus("error");
|
||||
setErrorMessage(
|
||||
err instanceof Error ? err.message : "Не удалось отправить сообщение"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (status === "success") {
|
||||
return (
|
||||
<div className="flex items-start gap-3 rounded-2xl border border-green-500/25 bg-green-500/[0.06] p-6">
|
||||
<CheckCircle2 className="mt-0.5 shrink-0 text-green-400" size={22} />
|
||||
<div>
|
||||
<p className="font-medium text-ink-100">Сообщение отправлено</p>
|
||||
<p className="mt-1 text-sm text-ink-500">
|
||||
Мы получили заявку и ответим в ближайшее время.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* Honeypot — скрыто от людей, видно ботам */}
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
className="absolute left-[-9999px] h-0 w-0 opacity-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label htmlFor="name" className="mb-1.5 block text-sm text-ink-300">
|
||||
Имя
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
required
|
||||
maxLength={100}
|
||||
placeholder="Как к вам обращаться"
|
||||
className="w-full rounded-xl border border-white/[0.08] bg-base-900/60 px-4 py-3 text-ink-100 placeholder:text-ink-700 focus:border-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="contact" className="mb-1.5 block text-sm text-ink-300">
|
||||
Telegram, WhatsApp или email
|
||||
</label>
|
||||
<input
|
||||
id="contact"
|
||||
name="contact"
|
||||
type="text"
|
||||
required
|
||||
maxLength={150}
|
||||
placeholder="Как с вами связаться"
|
||||
className="w-full rounded-xl border border-white/[0.08] bg-base-900/60 px-4 py-3 text-ink-100 placeholder:text-ink-700 focus:border-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="mb-1.5 block text-sm text-ink-300">
|
||||
Сообщение
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
maxLength={2000}
|
||||
rows={5}
|
||||
placeholder="Расскажите о задаче — направление, сроки, пожелания"
|
||||
className="w-full resize-none rounded-xl border border-white/[0.08] bg-base-900/60 px-4 py-3 text-ink-100 placeholder:text-ink-700 focus:border-blue-500/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === "error" && (
|
||||
<div className="flex items-start gap-2.5 rounded-xl border border-red-500/25 bg-red-500/[0.06] px-4 py-3 text-sm text-red-300">
|
||||
<AlertCircle size={18} className="mt-0.5 shrink-0" />
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "loading"}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-full bg-blue-500 px-6 py-3.5 font-medium text-white transition-transform hover:scale-[1.01] disabled:opacity-60 sm:w-auto"
|
||||
>
|
||||
{status === "loading" && <Loader2 size={18} className="animate-spin" />}
|
||||
{status === "loading" ? "Отправляем…" : "Отправить сообщение"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
interface FeatureBadgeProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export default function FeatureBadge({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
}: FeatureBadgeProps) {
|
||||
return (
|
||||
<div className="flex items-start gap-3.5">
|
||||
<div className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-white/[0.08] bg-white/[0.03] text-blue-400">
|
||||
<Icon size={18} strokeWidth={1.75} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-ink-100">{title}</p>
|
||||
<p className="text-sm text-ink-500">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import Link from "next/link";
|
||||
import { Send, Mail, MessageCircle } from "lucide-react";
|
||||
import Logo from "./Logo";
|
||||
import { navLinks } from "@/config/services";
|
||||
import { contacts } from "@/config/contacts";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-white/[0.06] px-6 py-14">
|
||||
<div className="mx-auto grid max-w-6xl gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<Logo />
|
||||
<p className="mt-4 max-w-xs text-sm leading-relaxed text-ink-500">
|
||||
Комплексные IT-решения для бизнеса: сайты, боты, CRM, серверы и
|
||||
техническая поддержка.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-4 font-mono text-xs uppercase tracking-[0.2em] text-ink-700">
|
||||
Разделы
|
||||
</p>
|
||||
<ul className="space-y-2.5">
|
||||
{navLinks.slice(1).map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-ink-500 transition-colors hover:text-ink-100"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-4 font-mono text-xs uppercase tracking-[0.2em] text-ink-700">
|
||||
Контакты
|
||||
</p>
|
||||
<ul className="space-y-2.5 text-sm text-ink-500">
|
||||
<li>{contacts.city}</li>
|
||||
<li>
|
||||
<a
|
||||
href={`mailto:${contacts.email}`}
|
||||
className="transition-colors hover:text-ink-100"
|
||||
>
|
||||
{contacts.email}
|
||||
</a>
|
||||
</li>
|
||||
<li>{contacts.workingHours}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-4 font-mono text-xs uppercase tracking-[0.2em] text-ink-700">
|
||||
Мы в сети
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href={contacts.telegramChannel}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Telegram"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-white/[0.08] text-ink-300 transition-colors hover:border-blue-500/40 hover:text-blue-400"
|
||||
>
|
||||
<Send size={17} />
|
||||
</a>
|
||||
<a
|
||||
href={contacts.vk}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="VKontakte"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-white/[0.08] text-ink-300 transition-colors hover:border-blue-500/40 hover:text-blue-400"
|
||||
>
|
||||
<MessageCircle size={17} />
|
||||
</a>
|
||||
<a
|
||||
href={`mailto:${contacts.email}`}
|
||||
aria-label="Email"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-white/[0.08] text-ink-300 transition-colors hover:border-blue-500/40 hover:text-blue-400"
|
||||
>
|
||||
<Mail size={17} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-12 max-w-6xl border-t border-white/[0.06] pt-6 text-xs text-ink-700">
|
||||
© {new Date().getFullYear()} LabNetHub. Все права защищены.
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Menu, X, ArrowUpRight } from "lucide-react";
|
||||
import Logo from "./Logo";
|
||||
import { navLinks } from "@/config/services";
|
||||
|
||||
export default function Header() {
|
||||
const pathname = usePathname();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="fixed inset-x-0 top-0 z-50 border-b border-white/[0.06] bg-base-950/80 backdrop-blur-lg">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
||||
<Link href="/" onClick={() => setOpen(false)}>
|
||||
<Logo />
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-1 lg:flex">
|
||||
{navLinks.map((link) => {
|
||||
const active =
|
||||
link.href === "/"
|
||||
? pathname === "/"
|
||||
: pathname.startsWith(link.href);
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`rounded-full px-4 py-2 text-sm transition-colors ${
|
||||
active
|
||||
? "bg-white/[0.06] text-ink-100"
|
||||
: "text-ink-500 hover:text-ink-100"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<Link
|
||||
href="/contacts"
|
||||
className="hidden shrink-0 items-center gap-1.5 rounded-full bg-blue-500 px-5 py-2.5 text-sm font-medium text-white transition-transform hover:scale-[1.03] lg:inline-flex"
|
||||
>
|
||||
Связаться
|
||||
<ArrowUpRight size={16} />
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full border border-white/[0.08] text-ink-100 lg:hidden"
|
||||
aria-label={open ? "Закрыть меню" : "Открыть меню"}
|
||||
aria-expanded={open}
|
||||
>
|
||||
{open ? <X size={20} /> : <Menu size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<nav className="border-t border-white/[0.06] bg-base-950 px-6 py-4 lg:hidden">
|
||||
<ul className="flex flex-col gap-1">
|
||||
{navLinks.map((link) => (
|
||||
<li key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
onClick={() => setOpen(false)}
|
||||
className="block rounded-lg px-3 py-2.5 text-ink-300 hover:bg-white/[0.05] hover:text-ink-100"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
interface LogoProps {
|
||||
className?: string;
|
||||
wordmarkClassName?: string;
|
||||
showWordmark?: boolean;
|
||||
}
|
||||
|
||||
export default function Logo({
|
||||
className = "h-8 w-8",
|
||||
wordmarkClassName = "text-lg",
|
||||
showWordmark = true,
|
||||
}: LogoProps) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-2.5 select-none">
|
||||
<svg
|
||||
viewBox="0 0 40 40"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="labnet-mark" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stopColor="#6FA8FF" />
|
||||
<stop offset="100%" stopColor="#2F63EB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon points="9,3 19,3 15,34 5,34" fill="#F5F7FB" />
|
||||
<polygon
|
||||
points="14,34 36,34 30,24 18,24"
|
||||
fill="url(#labnet-mark)"
|
||||
/>
|
||||
</svg>
|
||||
{showWordmark && (
|
||||
<span
|
||||
className={`font-display font-medium tracking-tight ${wordmarkClassName}`}
|
||||
>
|
||||
<span className="text-ink-100">LabNet</span>
|
||||
<span className="text-gradient-blue">Hub</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
interface NetworkGraphicProps {
|
||||
accent?: "blue" | "green";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const nodes = [
|
||||
{ x: 40, y: 40, r: 3 },
|
||||
{ x: 140, y: 20, r: 2.5 },
|
||||
{ x: 220, y: 70, r: 4 },
|
||||
{ x: 300, y: 30, r: 2.5 },
|
||||
{ x: 90, y: 130, r: 2.5 },
|
||||
{ x: 190, y: 150, r: 3.5 },
|
||||
{ x: 280, y: 130, r: 2.5 },
|
||||
{ x: 20, y: 190, r: 2 },
|
||||
{ x: 340, y: 190, r: 3 },
|
||||
];
|
||||
|
||||
const edges: [number, number][] = [
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[1, 4],
|
||||
[4, 5],
|
||||
[5, 6],
|
||||
[2, 5],
|
||||
[4, 7],
|
||||
[6, 8],
|
||||
[0, 4],
|
||||
];
|
||||
|
||||
export default function NetworkGraphic({
|
||||
accent = "blue",
|
||||
className = "",
|
||||
}: NetworkGraphicProps) {
|
||||
const stroke = accent === "green" ? "#12B76A" : "#3B7BFF";
|
||||
const dot = accent === "green" ? "#6EE7B7" : "#6FA8FF";
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 360 220"
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
>
|
||||
<g opacity="0.55">
|
||||
{edges.map(([a, b], i) => (
|
||||
<line
|
||||
key={i}
|
||||
x1={nodes[a].x}
|
||||
y1={nodes[a].y}
|
||||
x2={nodes[b].x}
|
||||
y2={nodes[b].y}
|
||||
stroke={stroke}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
{nodes.map((n, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
cx={n.x}
|
||||
cy={n.y}
|
||||
r={n.r}
|
||||
fill={dot}
|
||||
className="animate-pulse-slow"
|
||||
style={{ animationDelay: `${(i % 5) * 0.6}s` }}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import SectionEyebrow from "./SectionEyebrow";
|
||||
import NetworkGraphic from "./NetworkGraphic";
|
||||
|
||||
interface PageHeroProps {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
highlight?: string;
|
||||
subtitle: string;
|
||||
accent?: "blue" | "green";
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function PageHero({
|
||||
eyebrow,
|
||||
title,
|
||||
highlight,
|
||||
subtitle,
|
||||
accent = "blue",
|
||||
children,
|
||||
}: PageHeroProps) {
|
||||
return (
|
||||
<section className="relative overflow-hidden border-b border-white/[0.06] px-6 pb-16 pt-32 sm:pb-20 sm:pt-40">
|
||||
<div className="pointer-events-none absolute right-0 top-0 w-[420px] max-w-[60%] opacity-70 sm:opacity-100">
|
||||
<NetworkGraphic accent={accent} className="w-full" />
|
||||
</div>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<SectionEyebrow accent={accent}>{eyebrow}</SectionEyebrow>
|
||||
<h1 className="text-balance font-display text-4xl font-medium leading-[1.1] sm:text-5xl">
|
||||
{title}{" "}
|
||||
{highlight && (
|
||||
<span
|
||||
className={accent === "green" ? "text-gradient-green" : "text-gradient-blue"}
|
||||
>
|
||||
{highlight}
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="mt-5 max-w-xl text-lg leading-relaxed text-ink-500">
|
||||
{subtitle}
|
||||
</p>
|
||||
{children && <div className="mt-8 flex flex-wrap gap-3">{children}</div>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
interface SectionEyebrowProps {
|
||||
children: React.ReactNode;
|
||||
accent?: "blue" | "green";
|
||||
}
|
||||
|
||||
export default function SectionEyebrow({
|
||||
children,
|
||||
accent = "blue",
|
||||
}: SectionEyebrowProps) {
|
||||
const dot = accent === "green" ? "bg-green-400" : "bg-blue-400";
|
||||
return (
|
||||
<div className="mb-4 inline-flex items-center gap-2 font-mono text-xs uppercase tracking-[0.2em] text-ink-500">
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${dot}`} />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import Link from "next/link";
|
||||
import { Globe, Bot, Workflow, Server, LifeBuoy, ArrowUpRight } from "lucide-react";
|
||||
import type { ServiceMeta } from "@/config/services";
|
||||
|
||||
const iconMap = {
|
||||
Globe,
|
||||
Bot,
|
||||
Workflow,
|
||||
Server,
|
||||
LifeBuoy,
|
||||
};
|
||||
|
||||
interface ServiceCardProps {
|
||||
service: ServiceMeta;
|
||||
}
|
||||
|
||||
export default function ServiceCard({ service }: ServiceCardProps) {
|
||||
const Icon = iconMap[service.icon];
|
||||
const isGreen = service.accent === "green";
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={service.href}
|
||||
className={`group relative flex flex-col justify-between rounded-2xl border border-white/[0.06] bg-base-900/60 p-6 transition-all hover:-translate-y-1 clip-corner ${
|
||||
isGreen
|
||||
? "hover:border-green-500/30 hover:shadow-glow-green"
|
||||
: "hover:border-blue-500/30 hover:shadow-glow"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={`mb-5 flex h-11 w-11 items-center justify-center rounded-xl border ${
|
||||
isGreen
|
||||
? "border-green-500/25 bg-green-500/10 text-green-400"
|
||||
: "border-blue-500/25 bg-blue-500/10 text-blue-400"
|
||||
}`}
|
||||
>
|
||||
<Icon size={20} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 className="mb-2 font-display text-lg font-medium text-ink-100">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-ink-500">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center gap-1.5 text-sm font-medium text-ink-300 group-hover:text-ink-100">
|
||||
Подробнее
|
||||
<ArrowUpRight
|
||||
size={16}
|
||||
className="transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Единое место для всех контактов и ссылок на соцсети.
|
||||
// Замените placeholder-значения на реальные перед публикацией сайта.
|
||||
|
||||
export const contacts = {
|
||||
telegramBot: "https://t.me/labnethub_bot",
|
||||
telegramChannel: "https://t.me/labnethub",
|
||||
vk: "https://vk.com/labnethub",
|
||||
whatsapp: "https://wa.me/70000000000",
|
||||
email: "info@labnethub.ru",
|
||||
city: "Астрахань, Россия",
|
||||
workingHours: "Отвечаем в течение рабочего дня",
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
export type ServiceKey =
|
||||
| "websites"
|
||||
| "bots"
|
||||
| "crm"
|
||||
| "hosting"
|
||||
| "support";
|
||||
|
||||
export interface ServiceMeta {
|
||||
key: ServiceKey;
|
||||
href: string;
|
||||
icon: "Globe" | "Bot" | "Workflow" | "Server" | "LifeBuoy";
|
||||
title: string;
|
||||
description: string;
|
||||
accent: "blue" | "green";
|
||||
}
|
||||
|
||||
export const services: ServiceMeta[] = [
|
||||
{
|
||||
key: "websites",
|
||||
href: "/websites",
|
||||
icon: "Globe",
|
||||
title: "Сайты для бизнеса",
|
||||
description:
|
||||
"Сайты-визитки и полноценные корпоративные сайты — от идеи до запуска.",
|
||||
accent: "blue",
|
||||
},
|
||||
{
|
||||
key: "bots",
|
||||
href: "/bots",
|
||||
icon: "Bot",
|
||||
title: "Боты для соцсетей",
|
||||
description:
|
||||
"Telegram- и VK-боты: автоматизация заявок, уведомлений и продаж.",
|
||||
accent: "blue",
|
||||
},
|
||||
{
|
||||
key: "crm",
|
||||
href: "/crm",
|
||||
icon: "Workflow",
|
||||
title: "CRM-системы",
|
||||
description:
|
||||
"Установка, настройка и интеграция CRM с открытым исходным кодом.",
|
||||
accent: "blue",
|
||||
},
|
||||
{
|
||||
key: "hosting",
|
||||
href: "/hosting",
|
||||
icon: "Server",
|
||||
title: "Хостинг и серверы",
|
||||
description:
|
||||
"Размещение сайтов и ботов на наших серверах или у партнёров по всему миру.",
|
||||
accent: "green",
|
||||
},
|
||||
{
|
||||
key: "support",
|
||||
href: "/support",
|
||||
icon: "LifeBuoy",
|
||||
title: "IT-поддержка",
|
||||
description:
|
||||
"Обслуживание инфраструктуры, настройка оборудования, помощь 24/7.",
|
||||
accent: "blue",
|
||||
},
|
||||
];
|
||||
|
||||
export const navLinks = [
|
||||
{ href: "/", label: "Главная" },
|
||||
{ href: "/websites", label: "Сайты" },
|
||||
{ href: "/bots", label: "Боты" },
|
||||
{ href: "/crm", label: "CRM" },
|
||||
{ href: "/hosting", label: "Хостинг" },
|
||||
{ href: "/support", label: "Поддержка" },
|
||||
{ href: "/contacts", label: "Контакты" },
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const securityHeaders = [
|
||||
{ key: "X-DNS-Prefetch-Control", value: "on" },
|
||||
{
|
||||
key: "Strict-Transport-Security",
|
||||
value: "max-age=63072000; includeSubDomains; preload",
|
||||
},
|
||||
{ key: "X-Frame-Options", value: "DENY" },
|
||||
{ key: "X-Content-Type-Options", value: "nosniff" },
|
||||
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
||||
{
|
||||
key: "Permissions-Policy",
|
||||
value: "camera=(), microphone=(), geolocation=(), interest-cohort=()",
|
||||
},
|
||||
];
|
||||
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
poweredByHeader: false,
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: securityHeaders,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "labnethub",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^14.2.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"lucide-react": "^0.427.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.4",
|
||||
"@types/node": "^20.14.15",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.40",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.2.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
base: {
|
||||
950: "#050B18",
|
||||
900: "#0A1428",
|
||||
800: "#0F1B33",
|
||||
700: "#152442",
|
||||
600: "#1D2F52",
|
||||
},
|
||||
blue: {
|
||||
400: "#6FA8FF",
|
||||
500: "#3B7BFF",
|
||||
600: "#2F63EB",
|
||||
700: "#2450C4",
|
||||
},
|
||||
green: {
|
||||
300: "#6EE7B7",
|
||||
400: "#34D399",
|
||||
500: "#12B76A",
|
||||
600: "#0E9B5A",
|
||||
},
|
||||
ink: {
|
||||
100: "#F5F7FB",
|
||||
300: "#C4CDDD",
|
||||
500: "#93A1BD",
|
||||
700: "#5C6884",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
display: ["var(--font-unbounded)", "sans-serif"],
|
||||
body: ["var(--font-manrope)", "sans-serif"],
|
||||
mono: ["var(--font-jetbrains)", "monospace"],
|
||||
},
|
||||
backgroundImage: {
|
||||
"grid-fade":
|
||||
"radial-gradient(circle at 20% 20%, rgba(59,123,255,0.14), transparent 40%), radial-gradient(circle at 80% 0%, rgba(18,183,106,0.10), transparent 35%)",
|
||||
},
|
||||
boxShadow: {
|
||||
glow: "0 0 0 1px rgba(111,168,255,0.15), 0 20px 60px -20px rgba(47,99,235,0.35)",
|
||||
"glow-green":
|
||||
"0 0 0 1px rgba(110,231,183,0.15), 0 20px 60px -20px rgba(18,183,106,0.35)",
|
||||
},
|
||||
keyframes: {
|
||||
"pulse-slow": {
|
||||
"0%, 100%": { opacity: "0.35" },
|
||||
"50%": { opacity: "0.8" },
|
||||
},
|
||||
float: {
|
||||
"0%, 100%": { transform: "translateY(0px)" },
|
||||
"50%": { transform: "translateY(-8px)" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"pulse-slow": "pulse-slow 4s ease-in-out infinite",
|
||||
float: "float 6s ease-in-out infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user