Initial commit gov-llm-v2

This commit is contained in:
2026-02-04 00:04:31 +05:00
commit 07c4f48601
43 changed files with 7640 additions and 0 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
.git
backend-server

24
.gitignore vendored Executable file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

164
App.tsx Executable file
View File

@@ -0,0 +1,164 @@
import React, { useState, useEffect, useRef } from 'react';
import Hero from './components/Hero';
import Infrastructure from './components/Infrastructure';
import Science from './components/Science';
import Social from './components/Social';
import Leadership from './components/Leadership';
import Metrics from './components/Metrics';
import Scaling from './components/Scaling';
import Team from './components/Team';
import Timeline from './components/Timeline';
import BootAnimation from './components/BootAnimation';
import Modal from './components/Modal';
import { ModalData } from './types';
// import TechVisualizer from './components/TechVisualizer'; // Disabled per user request
import { ArrowUp } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
const App: React.FC = () => {
const [isBooting, setIsBooting] = useState(true);
const [showScrollTop, setShowScrollTop] = useState(false);
const [modalData, setModalData] = useState<ModalData | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Theme and Scroll Logic
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
const handleScroll = () => {
const scrollY = container.scrollTop;
const windowHeight = window.innerHeight;
const leadershipSection = document.getElementById('leadership'); // Block 4
const teamSection = document.getElementById('team');
// --- Theme Logic: Light mode starting from Leadership (Block 4) ---
if (leadershipSection && teamSection) {
// We use offsetTop relative to the container usually, but since sections are stacked
// and container scrolls, standard offsetTop works if container is relative/static.
const leadershipTrigger = leadershipSection.offsetTop - windowHeight * 0.6;
const teamTrigger = teamSection.offsetTop - windowHeight * 0.8;
// Active Light Mode for Leadership, Metrics, Scaling. Back to Dark for Team.
if (scrollY >= leadershipTrigger && scrollY < teamTrigger) {
document.body.classList.add('theme-light');
} else {
document.body.classList.remove('theme-light');
}
}
if (scrollY > 300) setShowScrollTop(true);
else setShowScrollTop(false);
};
container.addEventListener('scroll', handleScroll);
handleScroll(); // Trigger once on load
return () => {
container.removeEventListener('scroll', handleScroll);
document.body.classList.remove('theme-light');
};
}, [isBooting]);
const scrollToTop = () => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({ top: 0, behavior: 'smooth' });
}
};
const handleOpenModal = (data: ModalData) => {
setModalData(data);
};
const handleCloseModal = () => {
setModalData(null);
};
const handleContactClick = () => {
handleOpenModal({
title: 'Связь с разработчиком',
type: 'form',
theme: 'dark', // Keep header interaction dark themed typically
content: {
text: 'Оставьте свои контакты, и мы свяжемся с вами для обсуждения интеграции.'
}
});
};
return (
<div className="h-screen w-full bg-theme-main overflow-hidden relative">
<AnimatePresence>
{isBooting && <BootAnimation onComplete={() => setIsBooting(false)} />}
</AnimatePresence>
{!isBooting && (
<div className="relative h-full w-full">
{/* Grid Pattern Background */}
<div className="absolute inset-0 pointer-events-none grid-pattern z-0" />
{/* Header */}
<nav className="fixed top-0 left-0 right-0 z-[90] py-6 mix-blend-difference text-white pointer-events-none">
<div className="max-w-7xl mx-auto px-6 flex justify-between items-center pointer-events-auto">
<div className="font-mono text-sm font-bold tracking-widest flex items-center gap-3">
{/* Logo Icon */}
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-12 h-12">
<circle cx="100" cy="100" r="70" stroke="#ff4b4b" strokeWidth="6" fill="none" strokeDasharray="15 85"/>
<circle cx="100" cy="100" r="50" stroke="#ff4b4b" strokeWidth="6" fill="none" strokeDasharray="12 58" transform="rotate(30 100 100)"/>
<circle cx="100" cy="100" r="30" stroke="#ff4b4b" strokeWidth="6" fill="none" strokeDasharray="8 32" transform="rotate(60 100 100)"/>
</svg>
iiEasy <span className="opacity-50"> // Будущее.Просто.</span>
</div>
<button
onClick={handleContactClick}
className="text-xs font-mono font-bold hover:text-[#20e3b2] transition-colors uppercase cursor-pointer"
>
[ Связь ]
</button>
</div>
</nav>
<Timeline />
{/* Scroll Container */}
<main
ref={scrollContainerRef}
className="h-full w-full overflow-y-scroll snap-y snap-proximity lg:snap-mandatory snap-container scroll-smooth relative z-10"
>
<Hero id="hero" onOpenModal={handleOpenModal} />
<Infrastructure onOpenModal={handleOpenModal} />
<Science onOpenModal={handleOpenModal} />
<Social onOpenModal={handleOpenModal} />
<Leadership onOpenModal={handleOpenModal} />
<Metrics onOpenModal={handleOpenModal} />
<Scaling onOpenModal={handleOpenModal} />
<Team onOpenModal={handleOpenModal} />
</main>
{/* Global Modal */}
<AnimatePresence>
{modalData && <Modal data={modalData} onClose={handleCloseModal} />}
</AnimatePresence>
</div>
)}
<AnimatePresence>
{showScrollTop && !isBooting && !modalData && (
<motion.button
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
onClick={scrollToTop}
className="fixed bottom-8 right-8 z-50 p-4 bg-theme-main border border-theme text-theme-main hover:border-[#ff4b4b] transition-colors"
>
<ArrowUp className="w-5 h-5" />
</motion.button>
)}
</AnimatePresence>
</div>
);
};
export default App;

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Открываем порт, указанный в vite.config.ts
EXPOSE 4175
# Запускаем режим preview, который подхватит настройки прокси
CMD ["npm", "run", "preview"]

20
README.md Executable file
View File

@@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/drive/1O7g62BrH57dYM-58UFh_cntclmKjNNPu
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

8
backend-server/.env Executable file
View File

@@ -0,0 +1,8 @@
# Порт, на котором будет работать сервер
PORT=3001
# Ваш реальный ключ от Unisender
UNISENDER_API_KEY=6p4a3xc617jjaehi74c3tibxgg73gc56z8963y8a
# ID списка контактов в Unisender
UNISENDER_LIST_ID=1

82
backend-server/index.js Executable file
View File

@@ -0,0 +1,82 @@
const express = require('express');
const cors = require('cors');
const axios = require('axios');
const qs = require('qs');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3001;
// Разрешаем CORS, чтобы фронтенд мог обращаться к этому серверу
app.use(cors());
// Обработка JSON и URL-encoded тел запросов
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/api/unisender-proxy', async (req, res) => {
try {
console.log('➡️ Получен запрос от фронтенда');
// Проверка наличия ключа на сервере
if (!process.env.UNISENDER_API_KEY) {
console.error('❌ Ошибка: Не задан UNISENDER_API_KEY в файле .env');
return res.status(500).json({
error: 'Server Misconfiguration',
message: 'API Key not found on server'
});
}
// Берем данные, пришедшие с фронтенда
const incomingData = req.body;
// ИСПРАВЛЕНИЕ: Удаляем параметр 'overwrite', так как API Unisender (importContacts)
// выдает ошибку "This method should not contain extra fields" при его наличии.
// Метод importContacts обновляет существующие контакты по умолчанию.
if (incomingData.overwrite) {
delete incomingData.overwrite;
}
// Формируем payload для Unisender
const payload = {
...incomingData,
api_key: process.env.UNISENDER_API_KEY,
format: 'json'
};
console.log('🔄 Отправка данных в Unisender...');
// Отправляем запрос в Unisender
const response = await axios.post(
'https://api.unisender.com/ru/api/importContacts',
qs.stringify(payload), // Unisender требует x-www-form-urlencoded строку
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
const uniResponse = response.data;
if (uniResponse.error) {
console.error('❌ Ошибка Unisender API:', uniResponse);
return res.status(400).json(uniResponse);
}
console.log('✅ Успех:', uniResponse);
return res.json(uniResponse);
} catch (error) {
console.error('❌ Внутренняя ошибка сервера:', error.message);
return res.status(500).json({
error: 'Internal Server Error',
details: error.message
});
}
});
app.listen(PORT, () => {
console.log(`\n🚀 Proxy Server запущен на http://localhost:${PORT}`);
console.log(`🔧 Режим: ${process.env.NODE_ENV || 'development'}`);
});

1440
backend-server/package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

19
backend-server/package.json Executable file
View File

@@ -0,0 +1,19 @@
{
"name": "unisender-proxy",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
"dependencies": {
"axios": "^1.6.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"qs": "^6.11.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}

72
components/BootAnimation.tsx Executable file
View File

@@ -0,0 +1,72 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
const bootLogs = [
'ИНИЦИАЛИЗАЦИЯ СИСТЕМЫ...',
'ЗАГРУЗКА ЯДРА [##########......]',
'МОНТИРОВАНИЕ ТОМОВ...',
'ПОДКЛЮЧЕНИЕ К ЗАЩИЩЕННОЙ СЕТИ...',
'ДОСТУП РАЗРЕШЕН.'
];
interface Props {
onComplete: () => void;
}
const BootAnimation: React.FC<Props> = ({ onComplete }) => {
const [logs, setLogs] = useState<string[]>([]);
useEffect(() => {
let delay = 0;
bootLogs.forEach((log, index) => {
delay += 200 + Math.random() * 300;
setTimeout(() => {
setLogs(prev => [...prev, log]);
if (index === bootLogs.length - 1) {
setTimeout(onComplete, 800);
}
}, delay);
});
}, [onComplete]);
return (
<motion.div
exit={{ y: "-100%" }}
transition={{ duration: 0.8, ease: [0.76, 0, 0.24, 1] }}
className="fixed inset-0 z-[100] flex flex-col justify-between bg-[#0e0e10] p-8 md:p-12 text-[#20e3b2] font-mono text-xs uppercase"
>
<div className="flex justify-between w-full border-b border-[#20e3b2]/20 pb-4">
<span>iiEasy // ЗАГРУЗКА</span>
<span>V 1.0</span>
</div>
<div className="w-full max-w-lg">
{logs.map((log, i) => (
<motion.div
key={i}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="mb-1"
>
<span className="text-[#666] mr-2">
{String(i).padStart(2, '0')} ::
</span>
{log}
</motion.div>
))}
<motion.div
animate={{ opacity: [0, 1, 0] }}
transition={{ repeat: Infinity, duration: 0.2 }}
className="w-3 h-4 bg-[#20e3b2] mt-2 inline-block"
/>
</div>
<div className="flex justify-between w-full border-t border-[#20e3b2]/20 pt-4 text-[#666]">
<span>ПАМЯТЬ: 128TB</span>
<span>CPU: ОПТИМАЛЬНО</span>
</div>
</motion.div>
);
};
export default BootAnimation;

165
components/Hero.tsx Executable file
View File

@@ -0,0 +1,165 @@
import React from 'react';
import { motion } from 'framer-motion';
import { SectionProps } from '../types';
const Hero: React.FC<SectionProps> = ({ id, onOpenModal }) => {
// Grid config for the background animation
const GRID_ROWS = 10;
const GRID_COLS = 20;
const dots = Array.from({ length: GRID_ROWS * GRID_COLS });
const smoothEase: [number, number, number, number] = [0.645, 0.045, 0.355, 1.000];
const handleOpenSpecs = () => {
if (onOpenModal) {
onOpenModal({
title: 'Спецификация Системы v2.5.0-RC',
type: 'table',
theme: 'dark',
content: {
headers: ['Параметр', 'Значение', 'Описание'],
rows: [
['Архитектура', 'Transformer++ (MoE)', 'Гибридная архитектура Mixture of Experts для оптимизации инференса'],
['Параметры', '70B (Int8 Quantization)', 'Базовая модель, дообученная на закрытых датасетах РБ'],
['Контекстное окно', '128k токенов', 'Позволяет анализировать объемные юридические документы целиком'],
['Датасет', '50 лет архивов', 'Оцифрованные стенограммы заседаний, НПА, указы'],
['Развертывание', 'On-Premise (Air-gapped)', 'Полная изоляция от глобальной сети интернет'],
['Инференс', '20ms / token', 'Высокая скорость генерации на кластере H100']
]
}
});
}
};
const handleLaunch = () => {
if (onOpenModal) {
onOpenModal({
title: 'Запрос на развертывание',
type: 'form',
theme: 'dark',
content: {
text: 'Для запуска системы в вашем контуре требуется предварительное согласование архитектуры.'
}
});
}
};
return (
<section id={id} className="h-[100dvh] w-full flex flex-col items-center justify-between relative overflow-hidden bg-theme-main text-center px-4 snap-start shrink-0 border-b border-theme py-20 md:py-24">
{/* Wipe Effect for Hero (Reveals on load) */}
<motion.div
className="absolute inset-0 z-50 bg-[#ff4b4b] pointer-events-none"
initial={{ y: "0%" }}
animate={{ y: "-100%" }}
transition={{ duration: 0.8, ease: smoothEase, delay: 0.2 }}
/>
<motion.div
className="absolute inset-0 z-40 bg-[#20e3b2] pointer-events-none"
initial={{ y: "0%" }}
animate={{ y: "-100%" }}
transition={{ duration: 0.8, ease: smoothEase, delay: 0.3 }}
/>
{/* Animated Grid Background */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none opacity-20 overflow-hidden">
<div
className="grid gap-1"
style={{ gridTemplateColumns: `repeat(${GRID_COLS}, 1fr)` }}
>
{dots.map((_, i) => (
<motion.div
key={i}
initial={{ scale: 0, opacity: 0 }}
animate={{
scale: [0, 1, 0.5, 1],
opacity: [0, 0.8, 0.2, 0.5],
backgroundColor: Math.random() > 0.8 ? '#ff4b4b' : (Math.random() > 0.5 ? '#20e3b2' : '#333')
}}
transition={{
duration: 3,
repeat: Infinity,
repeatType: "reverse",
delay: Math.random() * 2,
ease: "easeInOut"
}}
className="w-4 h-4 md:w-8 md:h-8 rounded-sm"
/>
))}
</div>
</div>
<div className="flex-1 flex flex-col justify-center items-center z-10 max-w-5xl mx-auto relative w-full mt-8 md:mt-0">
<motion.div
initial={{ opacity: 0, scaleX: 0 }}
animate={{ opacity: 1, scaleX: 1 }}
transition={{ duration: 0.8, ease: "circOut", delay: 1 }}
className="flex justify-center mb-4 md:mb-8"
>
<div className="px-4 py-2 border border-[#20e3b2] bg-[#20e3b2]/10 text-[#20e3b2] font-mono text-[10px] md:text-xs font-bold tracking-widest uppercase">
Статус Системы: Онлайн
</div>
</motion.div>
<div className="relative overflow-hidden">
<motion.h1
initial={{ y: "100%" }}
animate={{ y: 0 }}
transition={{ duration: 1.2, ease: smoothEase, delay: 0.8 }}
className="text-[12vw] md:text-7xl lg:text-8xl xl:text-[8rem] font-black tracking-tighter text-theme-main leading-[0.85]"
>
СУВЕРЕННАЯ
</motion.h1>
</div>
<div className="relative overflow-hidden mb-6 md:mb-10">
<motion.h1
initial={{ y: "100%" }}
animate={{ y: 0 }}
transition={{ duration: 1.2, delay: 0.9, ease: smoothEase }}
className="text-[12vw] md:text-7xl lg:text-8xl xl:text-[8rem] font-black tracking-tighter text-[#ff4b4b] leading-[0.85]"
>
LLM СИСТЕМА
</motion.h1>
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.2, duration: 1 }}
className="flex flex-col items-center w-full"
>
<p className="text-sm md:text-base text-theme-muted max-w-3xl mx-auto mb-8 font-mono px-4 leading-relaxed">
<span className="text-theme-main">Башкортостан</span> создаст первый в России прецедент федерального масштаба: внедрения генеративного искусственного интеллекта в контур власти без рисков для национальной безопасности.
</p>
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto px-6 sm:px-0">
<button
onClick={handleLaunch}
className="w-full sm:w-auto px-6 py-4 bg-[#ff4b4b] text-white font-bold rounded-sm hover:bg-transparent hover:text-[#ff4b4b] border border-transparent hover:border-[#ff4b4b] transition-all duration-200 uppercase tracking-wider text-xs font-mono"
>
ЗАПУСТИТЬ
</button>
<button
onClick={handleOpenSpecs}
className="w-full sm:w-auto px-6 py-4 border border-theme text-theme-main font-bold rounded-sm hover:bg-theme-card transition-colors duration-200 uppercase tracking-wider text-xs font-mono"
>
СПЕЦИФИКАЦИИ
</button>
</div>
</motion.div>
</div>
<motion.div
className="w-full flex justify-between px-6 md:px-10 font-mono text-[10px] md:text-xs text-theme-muted z-10 pb-4 md:pb-0"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 2 }}
>
<span>ЛИСТАЙТЕ ВНИЗ</span>
<span className="hidden md:inline">ВЕРСИЯ 2.5.0-RC</span>
</motion.div>
</section>
);
};
export default Hero;

94
components/Infrastructure.tsx Executable file
View File

@@ -0,0 +1,94 @@
import React from 'react';
import SectionWrapper from './SectionWrapper';
import { Server, ShieldCheck, Cpu, Info } from 'lucide-react';
import { motion } from 'framer-motion';
import { SectionProps } from '../types';
const Infrastructure: React.FC<SectionProps> = ({ onOpenModal }) => {
const handleDetailsClick = () => {
if (onOpenModal) {
onOpenModal({
title: 'Техническая Спецификация ЦОД',
type: 'table',
theme: 'dark',
content: {
headers: ['Компонент', 'Характеристика', 'Примечания'],
rows: [
['Вычислительные узлы', 'GPU Кластер A100/H100', 'Собственность РБ'],
['СХД', 'All-Flash массивы', 'Скорость доступа <1мс'],
['Каналы связи', 'Защищенные ВОЛС', 'Изолированный контур'],
['Безопасность', 'ФСТЭК К1', 'Гостайна'],
['Энергоснабжение', 'Tier III', 'Резервирование 2N+1']
]
}
});
}
};
return (
<SectionWrapper id="infra" transitionEffect="wipe-right">
<div className="max-w-7xl mx-auto px-6 md:px-12 w-full h-full flex flex-col justify-center">
<div className="flex flex-col lg:flex-row gap-12 lg:gap-16 items-center lg:items-start justify-center">
<motion.div
className="w-full lg:w-1/2 flex flex-col justify-center"
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: [0.645, 0.045, 0.355, 1.000] }}
>
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 bg-[#20e3b2]"></div>
<span className="text-[#20e3b2] font-mono text-xs tracking-widest">01. ИНФРАСТРУКТУРА</span>
</div>
<h3 className="text-4xl md:text-6xl lg:text-7xl font-black mb-6 text-theme-main tracking-tighter leading-none">
АППАРАТНЫЙ<br/>
<span className="text-theme-muted">КОНТРОЛЬ</span>
</h3>
<p className="text-theme-muted text-base md:text-lg mb-8 leading-relaxed font-light max-w-md">
Интеллект Республики должен жить в её стенах. Стратегия цифрового суверенитета: отказ от аренды в пользу капитализации собственных активов. ЦОД станет "железным" сердцем Кампуса.
</p>
<motion.button
onClick={handleDetailsClick}
whileHover={{ x: 5 }}
className="flex items-center gap-2 text-[#20e3b2] font-mono text-sm uppercase hover:underline underline-offset-4 w-fit"
>
<Info className="w-4 h-4" />
Подробнее о мощностях
</motion.button>
</motion.div>
<div className="w-full lg:w-1/2 space-y-4 flex flex-col justify-center">
{[
{ icon: Server, title: "Фундамент на десятилетие", desc: "Архитектура дата-центра будет спроектирована на годы вперед." },
{ icon: ShieldCheck, title: "Контур Гостайны", desc: "Изолированный конвейер обработки данных по стандартам требованиий ФСТЭК." },
{ icon: Cpu, title: "Суверенный Кремний", desc: "Формирование суверенного вычислительного кластера (HPC) в собственности Республики." }
].map((item, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.2 + idx * 0.15, duration: 0.6, ease: [0.645, 0.045, 0.355, 1.000] }}
whileHover={{ x: -10 }}
className="flex items-start gap-4 md:gap-6 p-4 md:p-6 border-l border-theme bg-theme-card hover:bg-theme-card/80 hover:border-[#ff4b4b] transition-all duration-300 group cursor-default"
>
<div className="mt-1">
<item.icon className="w-5 h-5 text-[#ff4b4b]" />
</div>
<div>
<h4 className="text-theme-main font-bold text-lg md:text-xl mb-1 font-mono uppercase">{item.title}</h4>
<p className="text-theme-muted text-sm leading-relaxed">{item.desc}</p>
</div>
</motion.div>
))}
</div>
</div>
</div>
</SectionWrapper>
);
};
export default Infrastructure;

145
components/Leadership.tsx Executable file
View File

@@ -0,0 +1,145 @@
import React from 'react';
import SectionWrapper from './SectionWrapper';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
import { motion } from 'framer-motion';
import { ListPlus } from 'lucide-react';
import { SectionProps } from '../types';
// 1. Функция-помощник для окончаний (балл, балла, баллов)
const getNoun = (number: number, one: string, two: string, five: string) => {
let n = Math.abs(number);
n %= 100;
if (n >= 5 && n <= 20) {
return five;
}
n %= 10;
if (n === 1) {
return one;
}
if (n >= 2 && n <= 4) {
return two;
}
return five;
};
const data = [
{ name: 'Москва', value: 95 },
{ name: 'Белгородская', value: 94 },
{ name: 'Башкортостан', value: 93 },
{ name: 'Татарстан', value: 92 },
{ name: 'ХМАО', value: 90 },
];
const Leadership: React.FC<SectionProps> = ({ onOpenModal }) => {
const handleOpenRating = () => {
if (onOpenModal) {
onOpenModal({
title: 'Индекс цифровизации (ТОП-10)',
type: 'table',
theme: 'light',
content: {
headers: ['Позиция', 'Регион', 'Баллы', 'Динамика'],
rows: [
['1', 'Москва', '95.4', '+0.2'],
['2', 'Белгородская область', '94.1', '+1.5'],
['3', 'Республика Башкортостан', '93.8', '+4.2'],
['4', 'Республика Татарстан', '92.5', '-0.5'],
['5', 'ХМАО - Югра', '90.3', '+0.8'],
['6', 'Московская область', '88.9', '+1.1'],
['7', 'Санкт-Петербург', '87.4', '-1.2'],
['8', 'Тульская область', '86.2', '+0.5'],
['9', 'Калужская область', '85.1', '+0.3'],
['10', 'Челябинская область', '84.8', '+2.0']
]
}
});
}
};
return (
<SectionWrapper id="leadership" transitionEffect="paint-ripple">
<div className="max-w-7xl mx-auto px-6 md:px-12 w-full h-full flex flex-col justify-center">
<div className="flex flex-col lg:flex-row-reverse gap-12 lg:gap-16 items-center h-full">
<motion.div
className="w-full lg:w-1/2 flex flex-col justify-center"
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: [0.645, 0.045, 0.355, 1.000], delay: 0.8 }} // Delayed content to wait for ripple
>
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 bg-[#20e3b2]"></div>
<span className="text-[#20e3b2] font-mono text-xs tracking-widest">04. РЕЙТИНГ</span>
</div>
<h3 className="text-4xl md:text-6xl lg:text-7xl font-black text-theme-main mb-6 tracking-tighter leading-none">
IT ЛИДЕР <br/><span className="text-theme-muted">В РОССИИ</span>
</h3>
<p className="text-theme-muted text-base md:text-lg mb-8">
ОТ ЦИФРОВИЗАЦИИ К ИНТЕЛЛЕКТУАЛЬНОЙ ВЛАСТИ <br/> Башкортостан станет технологическим донором для регионов РФ. Мы сформируем федеральный стандарт ГосТехИИ.
</p>
<div className="flex gap-8 md:gap-12 font-mono mb-8">
<div>
<div className="text-3xl md:text-5xl font-bold text-theme-main mb-2">TOP-3</div>
<div className="text-xs text-theme-muted uppercase tracking-wider">Стратегия 2030</div>
</div>
<div>
<div className="text-3xl md:text-5xl font-bold text-[#20e3b2] mb-2">#1</div>
<div className="text-xs text-theme-muted uppercase tracking-wider">Приволжский ФО</div>
</div>
</div>
<button
onClick={handleOpenRating}
className="flex items-center gap-2 text-theme-main font-bold uppercase tracking-widest text-xs hover:text-[#20e3b2] transition-colors w-fit"
>
<ListPlus className="w-5 h-5" />
Развернуть рейтинг
</button>
</motion.div>
<motion.div
className="w-full lg:w-1/2 h-[300px] md:h-[400px] lg:h-[500px] bg-theme-card border border-theme p-4 md:p-8 relative"
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: 1.0, duration: 0.8, ease: [0.645, 0.045, 0.355, 1.000] }}
>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} layout="vertical" margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
<XAxis type="number" hide />
<YAxis
dataKey="name"
type="category"
tick={{ fill: 'var(--text-muted)', fontSize: 12, fontFamily: 'JetBrains Mono' }}
width={100}
axisLine={false}
tickLine={false}
/>
<Tooltip
separator=""
cursor={{fill: 'var(--bg-color)', opacity: 0.5}}
contentStyle={{ backgroundColor: 'var(--bg-color)', borderColor: 'var(--border-color)', color: 'var(--text-main)', fontFamily: 'JetBrains Mono' }}
itemStyle={{ color: 'var(--text-main)' }}
formatter={(value: any) => [
`${value} ${getNoun(Number(value), 'балл', 'балла', 'баллов')}`,
''
]}
/>
<Bar dataKey="value" barSize={20}>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.name === 'Башкортостан' ? '#20e3b2' : '#555'} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</motion.div>
</div>
</div>
</SectionWrapper>
);
};
export default Leadership;

32
components/MapSVG.tsx Executable file
View File

@@ -0,0 +1,32 @@
import React from 'react';
const MapSVG: React.FC = () => (
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 600" className="w-full h-full opacity-50" preserveAspectRatio="xMidYMid slice">
{/* Simple Abstract Map Data */}
<path
fill="currentColor"
d="M150,150 Q200,100 300,120 T450,100 T600,150 T750,120 T900,200 L950,400 Q850,500 700,450 T450,500 T200,450 Z"
opacity="0.1"
/>
<path
fill="none"
stroke="currentColor"
strokeWidth="0.5"
d="M150,150 Q200,100 300,120 T450,100 T600,150 T750,120 T900,200 L950,400 Q850,500 700,450 T450,500 T200,450 Z"
/>
{/* Decorative Grid Lines */}
<line x1="100" y1="100" x2="900" y2="100" stroke="currentColor" strokeWidth="0.5" opacity="0.2" strokeDasharray="4 4"/>
<line x1="100" y1="200" x2="900" y2="200" stroke="currentColor" strokeWidth="0.5" opacity="0.2" strokeDasharray="4 4"/>
<line x1="100" y1="300" x2="900" y2="300" stroke="currentColor" strokeWidth="0.5" opacity="0.2" strokeDasharray="4 4"/>
<line x1="100" y1="400" x2="900" y2="400" stroke="currentColor" strokeWidth="0.5" opacity="0.2" strokeDasharray="4 4"/>
<line x1="100" y1="500" x2="900" y2="500" stroke="currentColor" strokeWidth="0.5" opacity="0.2" strokeDasharray="4 4"/>
<line x1="200" y1="50" x2="200" y2="550" stroke="currentColor" strokeWidth="0.5" opacity="0.2" strokeDasharray="4 4"/>
<line x1="400" y1="50" x2="400" y2="550" stroke="currentColor" strokeWidth="0.5" opacity="0.2" strokeDasharray="4 4"/>
<line x1="600" y1="50" x2="600" y2="550" stroke="currentColor" strokeWidth="0.5" opacity="0.2" strokeDasharray="4 4"/>
<line x1="800" y1="50" x2="800" y2="550" stroke="currentColor" strokeWidth="0.5" opacity="0.2" strokeDasharray="4 4"/>
</svg>
);
export default MapSVG;

108
components/Metrics.tsx Executable file
View File

@@ -0,0 +1,108 @@
import React from 'react';
import SectionWrapper from './SectionWrapper';
import { motion } from 'framer-motion';
import { PieChart } from 'lucide-react';
import { SectionProps } from '../types';
const Metrics: React.FC<SectionProps> = ({ onOpenModal }) => {
const handleOpenReport = () => {
if (onOpenModal) {
onOpenModal({
title: 'Экономический Эффект',
type: 'article',
theme: 'light',
content: {
text: `Прогноз экономической эффективности внедрения Суверенной LLM к 2026 году:
1. **Оптимизация ФОТ**
Высвобождение до 30% рабочего времени сотрудников аппарата, занятых рутинной обработкой документов. Это эквивалентно экономии 1.2 млрд рублей в год.
2. **Импортозамещение ПО**
Отказ от зарубежных проприетарных решений (Microsoft 365 Copilot, Notion AI, Zoom AI) сохранит в бюджете порядка 400 млн рублей ежегодно.
3. **Ускорение процессов**
Сокращение сроков согласования НПА с 14 до 3 дней.
Ускорение обработки обращений граждан в 10 раз.
4. **Снижение ошибок**
Минимизация юридических рисков и штрафов за счет автоматической проверки документов на соответствие законодательству.`
}
});
}
};
return (
<SectionWrapper id="metrics" transitionEffect="sand">
<div className="max-w-7xl mx-auto px-6 md:px-12 w-full h-full flex flex-col justify-center items-center text-center">
{/* Header Section */}
<div className="mb-12 md:mb-20 flex flex-col items-center z-10">
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 bg-[#ff4b4b]"></div>
<span className="text-[#ff4b4b] font-mono text-xs tracking-widest">05. ЭКОНОМИКА</span>
</div>
<h3 className="text-4xl md:text-6xl lg:text-7xl font-black text-theme-main tracking-tighter uppercase leading-none">
ЭФФЕКТ<br/><span className="text-theme-muted">ВНЕДРЕНИЯ</span>
</h3>
<p className="text-theme-muted text-lg mb-8 max-w-2xl">
<br/> Внедрение суверенной LLM это конкретные экономические и управленческие метрики, которые должны быть достигнуты уже в среднесрочной перспективе.
</p>
<button
onClick={handleOpenReport}
className="inline-flex items-center gap-2 px-6 py-2 border border-theme text-theme-main hover:bg-theme-card transition-all rounded-full text-xs font-bold uppercase tracking-wide"
>
<PieChart className="w-4 h-4" />
Финансовая модель
</button>
</div>
{/* Metrics Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-12 w-full max-w-6xl z-10">
<MetricCard
value="8X"
label="Ускорение"
desc="Сокращение рутины при подготовке НПА, анализе стенограмм и входящей корреспонденции."
delay={0.2}
/>
<MetricCard
value="+15%"
label="Эффективность"
desc="Повышение исполнительской дисциплины по Нацпроектам."
delay={0.4}
/>
<MetricCard
value="0₽"
label="Стоимость Лицензий"
desc="Полное импортозамещение. Бюджетные средства инвестируются в региональную IT-отрасль."
delay={0.6}
/>
</div>
</div>
</SectionWrapper>
);
};
const MetricCard: React.FC<{value: string, label: string, desc: string, delay: number}> = ({ value, label, desc, delay }) => (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay, duration: 0.6 }}
whileHover={{ y: -5 }}
className="flex flex-col items-center justify-start pt-8 pb-8 px-4 border-t border-theme hover:bg-theme-card/50 transition-colors duration-300 w-full"
>
<div className="text-6xl md:text-8xl font-black mb-6 text-theme-main group-hover:text-[#ff4b4b] transition-colors duration-300 tracking-tighter">
{value}
</div>
<div className="text-xs font-bold text-theme-main mb-3 font-mono uppercase tracking-widest border-b border-theme/20 pb-2 inline-block">
{label}
</div>
<div className="text-theme-muted text-sm md:text-base leading-relaxed max-w-[200px]">
{desc}
</div>
</motion.div>
)
export default Metrics;

447
components/Modal.tsx Executable file
View File

@@ -0,0 +1,447 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Send, Check, Loader2, AlertCircle } from 'lucide-react';
import { ModalData } from '../types';
interface ModalProps {
data: ModalData;
onClose: () => void;
}
// --- КОНФИГУРАЦИЯ ДЛЯ ПРОДАКШЕНА ---
// В реальном проекте используйте переменные окружения (process.env.REACT_APP_...)
const UNISENDER_CONFIG = {
// Если true, запрос идет на ваш бэкенд (рекомендуется для PROD).
// Если false, запрос идет напрямую в Unisender (работает только если отключен CORS, для DEV).
USE_PROXY: true,
// URL вашего бэкенда, который перенаправит запрос в Unisender
PROXY_URL: '/api/unisender-proxy',
};
const formatText = (text: string, isLight: boolean) => {
const lines = text.split(/\r?\n/);
const elements: React.ReactNode[] = [];
let currentList: React.ReactNode[] = [];
const processInline = (str: string) => {
const parts = str.split(/(\*\*.*?\*\*)/g);
return parts.map((part, i) => {
if (part.startsWith('**') && part.endsWith('**')) {
return <strong key={i} className={`font-bold ${isLight ? 'text-black' : 'text-white'}`}>{part.slice(2, -2)}</strong>;
}
return part;
});
};
lines.forEach((line, i) => {
const trimmed = line.trim();
if (!trimmed) {
if (currentList.length > 0) {
elements.push(<ul key={`list-${i}`} className="list-disc pl-5 my-4 space-y-2 marker:text-current">{currentList}</ul>);
currentList = [];
}
return;
}
if (trimmed.startsWith('* ')) {
currentList.push(<li key={`li-${i}`} className="pl-1">{processInline(trimmed.slice(2))}</li>);
} else {
if (currentList.length > 0) {
elements.push(<ul key={`list-${i}`} className="list-disc pl-5 my-4 space-y-2 marker:text-current">{currentList}</ul>);
currentList = [];
}
elements.push(<p key={`p-${i}`} className="mb-4 last:mb-0 leading-relaxed">{processInline(trimmed)}</p>);
}
});
if (currentList.length > 0) {
elements.push(<ul key="list-end" className="list-disc pl-5 my-4 space-y-2 marker:text-current">{currentList}</ul>);
}
return elements;
};
const Modal: React.FC<ModalProps> = ({ data, onClose }) => {
const isLight = data.theme === 'light';
// Form State
const [formState, setFormState] = useState({ name: '', phone: '', email: '', honeypot: '' });
const [isSubmitted, setIsSubmitted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// Phone Mask Handler
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let input = e.target.value;
// Оставляем только цифры
let numbers = input.replace(/\D/g, '');
// Блокируем удаление префикса +7, если пользователь пытается стереть все
if (!numbers) {
setFormState(prev => ({ ...prev, phone: '' }));
return;
}
// Если ввод начинается с 7, 8 или 9 - считаем это российским номером
if (['7', '8', '9'].includes(numbers[0])) {
// Нормализация: если первая цифра 9, добавляем 7 в начало. Если 8 - меняем на 7.
if (numbers[0] === '9') numbers = '7' + numbers;
if (numbers[0] === '8') numbers = '7' + numbers.slice(1);
// Обрезаем до 11 символов (7 + 10 цифр)
numbers = numbers.slice(0, 11);
// Формирование маски +7 (XXX) XXX-XX-XX
let formatted = '+7';
if (numbers.length > 1) formatted += ' (' + numbers.slice(1, 4);
if (numbers.length >= 5) formatted += ') ' + numbers.slice(4, 7);
if (numbers.length >= 8) formatted += '-' + numbers.slice(7, 9);
if (numbers.length >= 10) formatted += '-' + numbers.slice(9, 11);
setFormState(prev => ({ ...prev, phone: formatted }));
} else {
// Для международных номеров просто добавляем +
setFormState(prev => ({ ...prev, phone: '+' + numbers }));
}
};
// Validation
const validateForm = () => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// Проверка телефона: должен содержать минимум 10 цифр.
// Российский номер в маске +7 (XXX) XXX-XX-XX содержит 11 цифр.
const phoneDigits = formState.phone.replace(/\D/g, '');
const isRussianLike = formState.phone.startsWith('+7');
if (!formState.name.trim()) return "Введите имя";
if (!emailRegex.test(formState.email)) return "Некорректный Email";
if (!formState.phone) return "Введите телефон";
if (isRussianLike && phoneDigits.length !== 11) return "Введите полный номер телефона";
if (phoneDigits.length < 10) return "Слишком короткий номер телефона";
return null;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setErrorMessage(null);
// 1. Anti-spam check (Honeypot)
if (formState.honeypot) {
// Бот заполнил скрытое поле, имитируем успех, но не отправляем
console.warn("Spam detected");
setIsSubmitted(true);
return;
}
// 2. Validation
const validationError = validateForm();
if (validationError) {
setErrorMessage(validationError);
return;
}
setIsSubmitting(true);
try {
const formData = new URLSearchParams();
// --- Логика формирования запроса для Unisender ---
// Если используем прокси, API ключ лучше добавлять на бэкенде, но здесь показан полный payload
formData.append('format', 'json');
formData.append('api_key', UNISENDER_CONFIG.API_KEY);
// Настройка полей
formData.append('field_names[0]', 'email');
formData.append('field_names[1]', 'phone');
formData.append('field_names[2]', 'Name');
formData.append('field_names[3]', 'email_list_ids');
// Значения полей
formData.append('data[0][0]', formState.email);
formData.append('data[0][1]', formState.phone);
formData.append('data[0][2]', formState.name);
formData.append('data[0][3]', UNISENDER_CONFIG.LIST_ID);
// Опции
// formData.append('double_optin', '3'); // Раскомментировать, если нужно письмо-подтверждение
// NOTE: Параметр 'overwrite' удален, так как он вызывает ошибку invalid_arg в методе importContacts
const targetUrl = UNISENDER_CONFIG.USE_PROXY
? UNISENDER_CONFIG.PROXY_URL
: UNISENDER_CONFIG.DIRECT_URL;
console.log(`Sending to: ${targetUrl} (Proxy Mode: ${UNISENDER_CONFIG.USE_PROXY})`);
const response = await fetch(targetUrl, {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
});
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const result = await response.json();
// Проверяем ошибки самого API Unisender
if (result.error) {
throw new Error(`Unisender Error: ${result.error} (code: ${result.code})`);
}
setIsSubmitted(true);
} catch (error: any) {
console.error('Submission error:', error);
// Удобное сообщение для разработчика/пользователя
if (error.message && error.message.includes('Failed to fetch')) {
setErrorMessage('Ошибка CORS или Сети. Для продакшена требуется серверный прокси, т.к. браузер блокирует прямые запросы к Unisender.');
} else {
setErrorMessage(error.message || 'Произошла ошибка при отправке данных.');
}
// --- DEV ONLY: Фейковый успех для демонстрации интерфейса, если ключи не настроены ---
if (UNISENDER_CONFIG.API_KEY === 'YOUR_PRODUCTION_API_KEY') {
console.warn("DEV MODE: Simulating success because API Key is generic.");
setTimeout(() => {
setErrorMessage(null);
setIsSubmitted(true);
}, 1500);
}
} finally {
setIsSubmitting(false);
}
};
return (
<div className={`fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-8 ${isLight ? 'text-[#1a1a1a]' : 'text-white'}`}>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/60 backdrop-blur-sm cursor-pointer"
/>
<motion.div
initial={{ scale: 0.9, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.9, opacity: 0, y: 20 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className={`relative w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-sm border shadow-2xl flex flex-col
${isLight
? 'bg-[#e3e1db] border-[#c4c3be]'
: 'bg-[#161618] border-[#333]'
}`}
>
<div className={`sticky top-0 z-10 px-6 py-4 border-b flex justify-between items-center
${isLight
? 'bg-[#e3e1db]/90 border-[#c4c3be]'
: 'bg-[#161618]/90 border-[#333]'
} backdrop-blur-md`}
>
<h2 className="text-xl md:text-3xl font-black uppercase tracking-tight">{data.title}</h2>
<button
onClick={onClose}
className={`p-2 rounded-full transition-colors ${isLight ? 'hover:bg-black/10' : 'hover:bg-white/10'}`}
>
<X className="w-6 h-6" />
</button>
</div>
<div className="p-6 md:p-10">
{data.type === 'article' && data.content.text && (
<div className={`prose max-w-none font-light ${isLight ? 'text-[#5c5c55]' : 'text-[#888899]'}`}>
{formatText(data.content.text, isLight)}
</div>
)}
{data.type === 'table' && data.content.headers && data.content.rows && (
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className={`border-b-2 ${isLight ? 'border-black/10' : 'border-white/10'}`}>
{data.content.headers.map((h, i) => (
<th key={i} className="py-3 px-4 font-mono text-sm uppercase opacity-70">{h}</th>
))}
</tr>
</thead>
<tbody>
{data.content.rows.map((row, i) => (
<tr key={i} className={`border-b ${isLight ? 'border-black/5 hover:bg-black/5' : 'border-white/5 hover:bg-white/5'} transition-colors`}>
{row.map((cell, j) => (
<td key={j} className="py-3 px-4">{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
{data.type === 'list' && data.content.items && (
<ul className="space-y-4">
{data.content.items.map((item, i) => (
<li key={i} className={`flex items-start gap-4 p-4 border rounded-sm ${isLight ? 'border-black/10 bg-black/5' : 'border-white/10 bg-white/5'}`}>
<span className={`font-mono text-xs mt-1 ${isLight ? 'text-[#008f72]' : 'text-[#20e3b2]'}`}>{(i + 1).toString().padStart(2, '0')} //</span>
<span className="text-lg">{item}</span>
</li>
))}
</ul>
)}
{data.type === 'form' && (
<div className="w-full max-w-2xl mx-auto">
<AnimatePresence mode="wait">
{!isSubmitted ? (
<motion.form
key="form"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onSubmit={handleSubmit}
className="space-y-6"
autoComplete="off"
>
<p className={`mb-8 ${isLight ? 'text-[#5c5c55]' : 'text-[#888899]'}`}>
{data.content.text || 'Заполните форму, и мы свяжемся с вами для обсуждения деталей внедрения.'}
</p>
{/* Honeypot Field (Invisible to humans) */}
<input
type="text"
name="details_confirm"
value={formState.honeypot}
onChange={(e) => setFormState({...formState, honeypot: e.target.value})}
style={{ display: 'none' }}
tabIndex={-1}
autoComplete="off"
/>
<div className="space-y-2">
<label className="text-xs font-mono uppercase tracking-widest opacity-70">ФИО *</label>
<input
type="text"
required
name="name"
disabled={isSubmitting}
value={formState.name}
onChange={(e) => setFormState({...formState, name: e.target.value})}
className={`w-full p-4 bg-transparent border rounded-sm outline-none focus:border-[#ff4b4b] transition-colors disabled:opacity-50
${isLight ? 'border-black/20 text-black' : 'border-white/20 text-white'}`}
placeholder="Иванов Иван Иванович"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-xs font-mono uppercase tracking-widest opacity-70">Телефон *</label>
<input
type="tel"
required
name="phone"
disabled={isSubmitting}
value={formState.phone}
onChange={handlePhoneChange}
className={`w-full p-4 bg-transparent border rounded-sm outline-none focus:border-[#ff4b4b] transition-colors disabled:opacity-50
${isLight ? 'border-black/20 text-black' : 'border-white/20 text-white'}`}
placeholder="+7 (999) 000-00-00"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-mono uppercase tracking-widest opacity-70">Email *</label>
<input
type="email"
required
name="email"
disabled={isSubmitting}
value={formState.email}
onChange={(e) => setFormState({...formState, email: e.target.value})}
className={`w-full p-4 bg-transparent border rounded-sm outline-none focus:border-[#ff4b4b] transition-colors disabled:opacity-50
${isLight ? 'border-black/20 text-black' : 'border-white/20 text-white'}`}
placeholder="corp@gov.rb.ru"
/>
</div>
</div>
{errorMessage && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="p-4 bg-red-500/10 border border-red-500/30 text-red-500 text-sm flex items-start gap-3 rounded-sm"
>
<AlertCircle className="w-5 h-5 shrink-0" />
<span>{errorMessage}</span>
</motion.div>
)}
<div className="pt-4">
<button
type="submit"
disabled={isSubmitting}
className={`flex items-center justify-center gap-3 w-full p-4 font-bold uppercase tracking-widest text-white transition-all
bg-[#ff4b4b] hover:bg-[#ff4b4b]/80 disabled:opacity-70 disabled:cursor-not-allowed`}
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Отправка...
</>
) : (
<>
Отправить заявку
<Send className="w-4 h-4" />
</>
)}
</button>
<p className="text-[10px] opacity-50 mt-4 text-center leading-normal">
Нажимая кнопку, вы подтверждаете согласие с <span className="underline cursor-pointer">Политикой обработки данных</span> и даете разрешение на коммуникацию.
</p>
</div>
</motion.form>
) : (
<motion.div
key="success"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col items-center justify-center text-center py-12"
>
<div className="w-20 h-20 bg-[#20e3b2]/20 rounded-full flex items-center justify-center mb-6 text-[#20e3b2]">
<Check className="w-10 h-10" />
</div>
<h3 className="text-2xl font-bold mb-2">Заявка принята</h3>
<p className={`${isLight ? 'text-[#5c5c55]' : 'text-[#888899]'}`}>
Успешно. <br/>Наши специалисты свяжутся с вами в ближайшее время.
</p>
<button
onClick={onClose}
className={`mt-8 px-8 py-3 border rounded-sm uppercase text-xs font-mono hover:bg-[#20e3b2] hover:text-white hover:border-[#20e3b2] transition-colors
${isLight ? 'border-black/20' : 'border-white/20'}`}
>
Закрыть окно
</button>
</motion.div>
)}
</AnimatePresence>
</div>
)}
</div>
<div className={`p-2 text-right text-[10px] font-mono opacity-40 uppercase tracking-widest
${isLight ? 'bg-black/5' : 'bg-white/5'}`}>
iiEasy Secure Modal // System v2.5
</div>
</motion.div>
</div>
);
};
export default Modal;

130
components/Scaling.tsx Executable file
View File

@@ -0,0 +1,130 @@
import React from 'react';
import SectionWrapper from './SectionWrapper';
import { motion } from 'framer-motion';
import MapSVG from './MapSVG';
import { Globe } from 'lucide-react';
import { SectionProps } from '../types';
const Scaling: React.FC<SectionProps> = ({ onOpenModal }) => {
const cities = [
{ x: 20, y: 35, label: 'КАЗАНЬ' },
{ x: 80, y: 45, label: 'ЕКАТЕРИНБУРГ' },
{ x: 70, y: 75, label: 'НОВОСИБИРСК' },
];
const handleOpenMap = () => {
if (onOpenModal) {
onOpenModal({
title: 'Стратегия Масштабирования',
type: 'list',
theme: 'light',
content: {
items: [
'Этап 1 (2025): Пилотное внедрение в Башкортостане.',
'Этап 2 (2026): Тиражирование в ПФО (Татарстан, Самара).',
'Этап 3 (2027): Выход на федеральный уровень, интеграция с платформой Гостех.',
'Экспорт: Поставка коробочных решений для стран БРИКС.',
]
}
});
}
};
return (
<SectionWrapper id="scaling" transitionEffect="slide">
<div className="max-w-7xl mx-auto px-6 md:px-12 w-full flex-grow flex flex-col justify-center">
<div className="flex flex-col lg:flex-row gap-12 lg:gap-16 items-center h-full">
<motion.div
className="w-full lg:w-1/2 flex flex-col justify-center h-auto"
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: [0.645, 0.045, 0.355, 1.000] }}
>
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 bg-[#000000]"></div>
<span className="text-[#0000000] font-mono text-xs tracking-widest">06. МАСШТАБИРОВАНИЕ</span>
</div>
<h3 className="text-4xl md:text-6xl lg:text-7xl font-black text-theme-main mb-8 tracking-tighter leading-none">
ЭКСПОРТНЫЙ <br/> <span className="text-theme-muted">ПОТЕНЦИАЛ</span>
</h3>
<p className="text-theme-muted text-lg mb-8">
Cоздим продукт с высокой добавленной стоимостью. Успешный опыт Башкортостана будет упакован для масштабирования на уровне страны.
</p>
<ul className="space-y-4 font-mono text-xs text-theme-main uppercase tracking-wide mb-8">
<li className="flex items-center gap-3">
<span className="text-[#ff4b4b]">&gt;&gt;</span> Федеральный Масштаб
</li>
<li className="flex items-center gap-3">
<span className="text-[#ff4b4b]">&gt;&gt;</span> Новая статья доходов в республике
</li>
</ul>
<button
onClick={handleOpenMap}
className="flex items-center gap-3 text-theme-main font-bold uppercase tracking-widest text-xs border border-theme px-6 py-3 rounded-sm hover:bg-theme-main hover:text-white transition-all w-fit group"
>
<Globe className="w-4 h-4 group-hover:rotate-12 transition-transform" />
Карта Экспансии
</button>
</motion.div>
<motion.div
className="w-full lg:w-1/2 relative h-[300px] md:h-[400px] lg:h-[500px] border border-theme bg-theme-card overflow-hidden"
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.2, duration: 0.8, ease: [0.645, 0.045, 0.355, 1.000] }}
>
{/* SVG Map Container */}
<div className="absolute inset-0 flex items-center justify-center opacity-30 mix-blend-multiply pointer-events-none">
<div className="w-full h-full p-8 grayscale contrast-125">
<MapSVG />
</div>
</div>
{/* Animated Connections */}
<svg className="absolute inset-0 w-full h-full z-10 pointer-events-none">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="10" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#ff4b4b" />
</marker>
</defs>
{cities.map((city, i) => (
<motion.line
key={i}
x1="50%" y1="50%"
x2={`${city.x}%`} y2={`${city.y}%`}
stroke="#ff4b4b"
strokeWidth="1"
strokeDasharray="4 4"
initial={{ pathLength: 0, opacity: 0 }}
whileInView={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 1.5, delay: 0.5 + i * 0.3, ease: [0.645, 0.045, 0.355, 1.000] }}
/>
))}
</svg>
{/* Cities */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-4 h-4 bg-theme-main rounded-full z-20 shadow-[0_0_15px_currentColor]"></div>
{cities.map((city, i) => (
<motion.div
key={i}
initial={{ opacity: 0, scale: 0 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ delay: 1 + i * 0.3, ease: "backOut" }}
className="absolute z-20 w-3 h-3 bg-[#ff4b4b] rounded-full"
style={{ left: `${city.x}%`, top: `${city.y}%`, transform: 'translate(-50%, -50%)' }}
>
<span className="absolute top-4 left-1/2 -translate-x-1/2 font-mono text-[10px] text-theme-main bg-theme-card px-1 border border-theme">{city.label}</span>
</motion.div>
))}
</motion.div>
</div>
</div>
</SectionWrapper>
);
};
export default Scaling;

117
components/Science.tsx Executable file
View File

@@ -0,0 +1,117 @@
import React from 'react';
import SectionWrapper from './SectionWrapper';
import { motion } from 'framer-motion';
import { GraduationCap, Network, Microscope, FileText } from 'lucide-react';
import { SectionProps } from '../types';
const Science: React.FC<SectionProps> = ({ onOpenModal }) => {
const handleOpenProgram = (e: React.MouseEvent) => {
e.stopPropagation();
if (onOpenModal) {
onOpenModal({
title: 'Научно-образовательная программа',
type: 'article',
theme: 'dark',
content: {
text: `В рамках Евразийского НОЦ разворачивается масштабная программа по подготовке кадров новой формации.
1. **Кафедра прикладного ИИ**
Создание специализированных магистерских программ, ориентированных на решение задач госуправления. Студенты будут проходить практику на реальных обезличенных данных ведомств.
2. **Лаборатория NLP**
Фокус на обработке естественного языка для автоматизации документооборота. Разработка собственных языковых моделей, дообученных на массивах нормативно-правовых актов РФ и РБ.
3. **Грантовая поддержка**
Выделение грантов молодым ученым, работающим над алгоритмами оптимизации городских процессов и предиктивной аналитикой социальных рисков.
Цель: Полностью закрыть потребность региона в специалистах по Data Science и Machine Learning за счет внутренних ресурсов к 2027 году.`
}
});
}
};
return (
<SectionWrapper id="science" transparent={false} transitionEffect="warp">
<div className="max-w-7xl mx-auto px-6 md:px-12 w-full h-full flex flex-col justify-center z-10 relative">
{/* Left Aligned Text Block */}
<motion.div
className="w-full max-w-4xl flex flex-col items-start text-left mb-12 relative z-20"
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: [0.645, 0.045, 0.355, 1.000], delay: 0.5 }}
>
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 bg-[#ff4b4b]"></div>
<span className="text-[#ff4b4b] font-mono text-xs tracking-widest">02. НАУКА</span>
</div>
<h3 className="text-4xl md:text-6xl lg:text-7xl font-black text-theme-main mb-6 tracking-tighter leading-none">
ЦЕНТР ИИ <br/><span className="text-theme-muted">В ЕВРАЗИЙСКОМ НОЦ</span>
</h3>
<p className="text-theme-muted text-base md:text-lg font-light leading-relaxed mb-8 max-w-lg">
Фундаментальная научно-исследовательская работа, в тесной интеграции с Евразийским НОЦ мирового уровня, объединяя усилия ведущих вузов Уфы, дополняя экосистему Евразийского НОЦ.
</p>
<div className="flex flex-wrap items-center gap-6 relative z-50 pointer-events-auto">
<button
onClick={handleOpenProgram}
className="group flex items-center gap-3 px-6 py-3 bg-theme-card border border-theme text-theme-main hover:border-[#ff4b4b] hover:text-[#ff4b4b] transition-all duration-300 font-mono text-xs uppercase cursor-pointer shadow-lg active:scale-95"
>
<FileText className="w-4 h-4 group-hover:scale-110 transition-transform" />
Читать программу
</button>
<div className="inline-block p-4 border border-[#20e3b2]/30 bg-[#20e3b2]/5 rounded-sm font-mono text-xs text-[#20e3b2] max-w-xs break-words">
&gt; Соединение с базой данных... <span className="animate-pulse">_</span>
</div>
</div>
</motion.div>
{/* 3-Column Grid */}
<div className="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 content-start max-w-6xl relative z-10">
{[
{
icon: GraduationCap,
title: "Академическая База",
text: "Важно, чтобы Кампус стал полигоном, где изучение ИИ напрямую влияет на сектор экономики и управления."
},
{
icon: Network,
title: "Центр Компетенций",
text: "Компетенции разработки и обучения закрепятся за уфимской научной школой, а не останутся у вендора."
},
{
icon: Microscope,
title: "НИОКР Формат",
text: "Используем надежную технологию обучения нейросетей. Это исключает ошибки и сделает работу системы предсказуемой."
}
].map((card, i) => (
<motion.div
key={i}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.7 + i * 0.1, duration: 0.6, ease: [0.645, 0.045, 0.355, 1.000] }}
whileHover={{ y: -10 }}
className="flex flex-col items-start text-left p-6 md:p-8 border border-theme bg-theme-card/80 backdrop-blur-sm relative overflow-hidden group hover:border-[#20e3b2] transition-colors duration-300 h-full cursor-default"
>
<div className="mb-4 p-3 bg-theme-main/50 rounded-full group-hover:bg-[#20e3b2]/10 transition-colors duration-300">
<card.icon className="w-8 h-8 text-[#20e3b2]" />
</div>
<h4 className="text-lg md:text-xl font-bold text-theme-main font-mono uppercase mb-3">{card.title}</h4>
<p className="text-theme-muted leading-relaxed text-sm">{card.text}</p>
</motion.div>
))}
</div>
</div>
</SectionWrapper>
);
};
export default Science;

207
components/SectionWrapper.tsx Executable file
View File

@@ -0,0 +1,207 @@
import React, { useRef } from 'react';
import { motion, useScroll, useTransform, Variants, useInView } from 'framer-motion';
import { WarpOverlay, SandOverlay } from './VisualEffects';
export type TransitionType =
| 'warp'
| 'wipe-right'
| 'shutters'
| 'paint-ripple'
| 'sand'
| 'slide'
| 'gradient';
interface Props {
children: React.ReactNode;
id: string;
className?: string;
transparent?: boolean;
transitionEffect?: TransitionType;
fitScreen?: boolean;
}
const SectionWrapper: React.FC<Props> = ({
children,
id,
className = "",
transparent = false,
transitionEffect = 'slide',
fitScreen = true
}) => {
const ref = useRef<HTMLElement>(null);
const isInView = useInView(ref, { amount: 0.2 });
// Parallax Logic
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
});
const y = useTransform(scrollYProgress, [0, 1], ["5%", "-5%"]);
const smoothEase: [number, number, number, number] = [0.645, 0.045, 0.355, 1.000];
// --- RENDER OVERLAYS BASED ON TYPE ---
const renderOverlay = () => {
switch (transitionEffect) {
case 'warp':
return (
<motion.div
className="absolute inset-0 z-50 pointer-events-none"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.5 }}
>
{/* Only render WarpOverlay when in view to restart the animation/canvas clock */}
{isInView && <WarpOverlay />}
<motion.div
className="absolute inset-0 bg-[#0e0e10]"
initial={{ opacity: 1 }}
whileInView={{ opacity: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
/>
</motion.div>
);
case 'wipe-right':
return (
<>
<motion.div className="absolute inset-0 z-50 bg-[#ff4b4b] pointer-events-none"
initial={{ x: "0%" }} whileInView={{ x: "100%" }} viewport={{ once: true, amount: 0.2 }} transition={{ duration: 0.7, ease: smoothEase }} />
<motion.div className="absolute inset-0 z-40 bg-[#20e3b2] pointer-events-none"
initial={{ x: "0%" }} whileInView={{ x: "100%" }} viewport={{ once: true, amount: 0.2 }} transition={{ duration: 0.7, ease: smoothEase, delay: 0.1 }} />
</>
);
case 'shutters':
return (
<div className="absolute inset-0 z-50 pointer-events-none flex flex-row">
{[0, 1, 2, 3].map((i) => (
<motion.div
key={i}
className={`relative h-full w-1/4 bg-[#1c1c1f] border-x border-[#333]/50 ${i % 2 === 0 ? 'border-b-4 border-b-[#20e3b2]' : 'border-t-4 border-t-[#ff4b4b]'}`}
initial={{ scaleY: 1 }}
whileInView={{ scaleY: 0 }}
viewport={{ once: true, amount: 0.2 }}
transition={{
duration: 0.8,
ease: [0.76, 0, 0.24, 1], // expoOut
delay: i * 0.05
}}
style={{ originY: i % 2 === 0 ? 0 : 1 }} // Evens shrink to top, Odds shrink to bottom
/>
))}
</div>
);
case 'paint-ripple':
return (
<>
{/* The Drop */}
<motion.div
className="absolute left-1/2 top-0 z-[60] w-3 h-12 bg-white rounded-full pointer-events-none -translate-x-1/2"
initial={{ y: "-10vh", opacity: 1 }}
whileInView={{ y: "50vh", opacity: [1, 1, 0] }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.6, ease: "easeIn" }}
/>
{/* The Ripple */}
<motion.div
className="absolute inset-0 z-0 bg-[#e3e1db] pointer-events-none flex items-center justify-center overflow-hidden"
initial={{ clipPath: "circle(0% at 50% 50%)" }}
whileInView={{ clipPath: "circle(150% at 50% 50%)" }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.8, ease: "circOut", delay: 0.55 }}
/>
</>
);
case 'gradient':
return (
<motion.div
className="absolute inset-0 z-[20] pointer-events-none"
style={{
background: "linear-gradient(to bottom, #e3e1db 0%, transparent 100%)"
}}
initial={{ opacity: 1 }}
whileInView={{ opacity: 0 }}
viewport={{ once: true, amount: 0.1 }} // Trigger almost immediately on mobile to prevent blocking text
transition={{ duration: 1.5 }}
/>
);
case 'sand':
return (
<motion.div
className="absolute inset-0 z-0 overflow-hidden"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 1 }}
>
<SandOverlay />
</motion.div>
)
case 'slide':
return (
<motion.div
className="absolute inset-0 z-50 bg-[#ebe9e4] pointer-events-none"
initial={{ y: "0%" }}
whileInView={{ y: "100%" }}
viewport={{ once: true, amount: 0.01 }} // Trigger almost immediately
transition={{ duration: 1.0, ease: [0.76, 0, 0.24, 1] }}
/>
);
default:
return null;
}
};
// --- CONTENT VARIANTS ---
const getContentVariants = (): Variants | undefined => {
if (transitionEffect === 'sand') {
return {
hidden: { opacity: 0, scale: 0.95, filter: "blur(10px)" },
visible: {
opacity: 1,
scale: 1,
filter: "blur(0px)",
transition: { duration: 1.2, ease: "easeOut" }
}
};
}
return undefined;
};
return (
<section
ref={ref}
id={id}
className={`w-full relative overflow-hidden snap-start shrink-0 flex flex-col
min-h-[100dvh]
${transparent ? 'bg-transparent' : 'bg-theme-main'}
${transitionEffect === 'gradient' ? 'bg-[#0e0e10]' : ''}
border-b border-theme/20 ${className}`}
>
{renderOverlay()}
<motion.div
style={transitionEffect !== 'slide' ? { y } : undefined}
initial={transitionEffect === 'sand' ? "hidden" : undefined}
whileInView={transitionEffect === 'sand' ? "visible" : undefined}
viewport={{ once: true, amount: 0.2 }}
variants={getContentVariants()}
className="w-full flex-grow flex flex-col items-center justify-center px-4 md:px-12 py-20 md:py-28 relative z-10"
>
{children}
</motion.div>
</section>
);
};
export default SectionWrapper;

121
components/Social.tsx Executable file
View File

@@ -0,0 +1,121 @@
import React from 'react';
import SectionWrapper from './SectionWrapper';
import { HeartHandshake, AlertTriangle, MessageCircle, BarChart2, FileText } from 'lucide-react';
import { motion } from 'framer-motion';
import { SectionProps } from '../types';
const Social: React.FC<SectionProps> = ({ onOpenModal }) => {
const handleOpenMethodology = () => {
if (onOpenModal) {
onOpenModal({
title: 'Методика Предиктивной Аналитики',
type: 'list',
theme: 'dark',
content: {
items: [
'Агрегация данных из 50+ открытых источников (соцсети, СМИ, блоги).',
'Семантический анализ тональности сообщений (Sentiment Analysis).',
'Выявление очагов социальной напряженности на ранней стадии.',
'Автоматическая классификация обращений по категориям (ЖКХ, Дороги, Медицина).',
'Формирование тепловых карт проблемных зон региона.'
]
}
});
}
};
return (
<SectionWrapper id="social" transitionEffect="shutters">
<div className="max-w-7xl mx-auto px-6 md:px-12 w-full h-full flex flex-col justify-center">
<div className="flex flex-col lg:flex-row gap-12 lg:gap-16 items-center h-full">
<motion.div
className="w-full lg:w-1/2 order-2 lg:order-1"
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: [0.645, 0.045, 0.355, 1.000] }}
>
{/* Tech Grid Visualization */}
<div className="grid grid-cols-8 gap-1 p-2 bg-theme-card border border-theme">
{Array.from({ length: 64 }).map((_, i) => {
const active = Math.random() > 0.85;
return (
<motion.div
key={i}
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
transition={{ delay: Math.random() * 0.5, duration: 0.5 }}
className={`aspect-square ${active ? 'bg-[#ff4b4b]' : 'bg-[#333]/20'} rounded-sm`}
/>
)
})}
</div>
<div className="flex justify-between mt-4 font-mono text-[10px] text-theme-muted uppercase">
<span>Статус: Мониторинг</span>
<span className="text-[#ff4b4b] animate-pulse">Угрозы: Обнаружены</span>
</div>
</motion.div>
<motion.div
className="w-full lg:w-1/2 order-1 lg:order-2 flex flex-col justify-center"
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: [0.645, 0.045, 0.355, 1.000] }}
>
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 bg-[#ffffff]"></div>
<span className="text-theme-main font-mono text-xs tracking-widest">03. ВЛИЯНИЕ</span>
</div>
<h3 className="text-4xl md:text-6xl lg:text-7xl font-black mb-6 md:mb-8 text-theme-main tracking-tighter leading-none">
ЦИФРОВОЙ<br/>
<span className="text-theme-muted">ЩИТ</span>
</h3>
<p className="text-theme-muted text-base md:text-lg font-light leading-relaxed mb-8 max-w-lg">
Главный показателем системы социальное благополучие и доверие граждан. ИИ станет главным аналитическим фильтром ЦУР, работающим на упреждение рисков.
</p>
<div className="mb-8">
<button
onClick={handleOpenMethodology}
className="flex items-center gap-3 px-6 py-3 border border-theme text-theme-main font-bold uppercase tracking-widest text-xs hover:bg-[#ff4b4b] hover:text-white hover:border-[#ff4b4b] transition-all duration-300 w-fit group active:scale-95"
>
<FileText className="w-4 h-4" />
Отчет по рискам
</button>
</div>
<div className="space-y-4">
{[
{ icon: MessageCircle, title: "Слышать Каждого", desc: "Мгновенная обработка обращений граждан." },
{ icon: AlertTriangle, title: "Предвидеть Риски", desc: "Мониторинг и прогнозирование социальной напряженности." },
{ icon: HeartHandshake, title: "Поддержка Населения", desc: "Отслеживания получения льгот для граждан." }
].map((item, idx) => (
<motion.div
key={idx}
initial={{ x: 20, opacity: 0 }}
whileInView={{ x: 0, opacity: 1 }}
transition={{ delay: idx * 0.1 + 0.3 }}
// lg:hover:pl-6 prevents jumpiness on touch scroll, active state adds feedback
className="flex gap-4 p-4 border-b border-theme/30 lg:hover:pl-6 active:bg-theme-card/50 transition-all duration-300 cursor-default group"
>
<div className="mt-1 opacity-50 group-hover:opacity-100 transition-opacity">
<item.icon className="w-5 h-5 text-theme-main" />
</div>
<div>
<h4 className="text-theme-main font-bold text-lg font-mono uppercase">{item.title}</h4>
<p className="text-theme-muted text-sm">{item.desc}</p>
</div>
</motion.div>
))}
</div>
</motion.div>
</div>
</div>
</SectionWrapper>
);
};
export default Social;

279
components/Team.tsx Executable file
View File

@@ -0,0 +1,279 @@
import React from 'react';
import SectionWrapper from './SectionWrapper';
import { ArrowRight } from 'lucide-react';
import { motion } from 'framer-motion';
import { SectionProps } from '../types';
interface Member {
id: number;
name: string;
role: string;
bio: string;
image: string;
}
const members: Member[] = [
{
id: 1,
name: 'Ахметзянов Арсен',
role: 'CEO / Генеральный директор',
bio: 'Более 15 лет в IT-индустрии. Руководил внедрением крупных цифровых платформ. Автор стратегии развития суверенных ИИ-решений для госсектора.',
image: '/media/team/arsen.jpg'
},
{
id: 2,
name: 'Мирсаяпов Эдгар',
role: 'CCO / Коммерческий директор',
bio: 'Эксперт по стратегическому развитию бизнеса. Отвечает за коммерциализацию продукта, взаимодействие с партнерами и масштабирование на федеральном уровне.',
image: '/media/team/edgar.jpg'
},
{
id: 3,
name: 'Решетников Александр',
role: 'CTO / Технический директор',
bio: 'Архитектор высоконагруженных систем. Специализируется на проектировании безопасной инфраструктуры ЦОД и оптимизации нейросетевых моделей.',
image: '/media/team/alexandr.jpg'
},
{
id: 4,
name: 'Зулькарнаев Даниэль',
role: 'CPO / Менеджер проекта',
bio: 'Управляет жизненным циклом продукта. Координирует работу команд разработки и внедрения, обеспечивая соответствие функционала требованиям заказчика.',
image: '/media/team/daniel.jpg'
}
];
const PRIVACY_POLICY = `
**ПОЛИТИКА ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ ООО «ИЗИ ГРУПП»**
**I. Общие положения**
1.1. Настоящая Политика обработки персональных данных (далее Политика) разработана в соответствии с требованиями Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных» (далее ФЗ № 152) и определяет порядок обработки персональных данных (ПДн) и меры по обеспечению безопасности ПДн в Обществе с ограниченной ответственностью «ИЗИ ГРУПП» (далее Оператор).
1.2. Оператор осуществляет обработку ПДн с целью обеспечения соблюдения законов РФ, а также с целью осуществления своей основной деятельности: предоставление информации о продуктах и услугах, консультирование, ответ на обращения пользователей, а также заключение и исполнение договоров.
1.3. Действие Политики распространяется на все процессы Оператора, связанные с обработкой ПДн.
**II. Сведения об Операторе**
Наименование: Общество с ограниченной ответственностью «ИЗИ ГРУПП» (ООО «ИЗИ ГРУПП»)
Адрес: Респ. Башкортостан, г. Уфа, пр-кт Октября, д. 107а, кв. 436.
ИНН: 0277953363
КПП: 027701001
Сайт: gov.iieasy.ru
**III. Принципы и цели обработки ПДн**
3.1. **Принципы обработки ПДн:** Обработка ПДн осуществляется на законной и справедливой основе, ограничивается достижением конкретных, заранее определенных и законных целей. Содержание и объем обрабатываемых ПДн соответствуют заявленным целям обработки.
3.2. **Цели обработки ПДн:**
* Установление обратной связи с пользователем по его запросу, инициированному через предоставленные каналы связи (в том числе через мессенджеры).
* Предоставление информации о продуктах, экспертизе и услугах Оператора.
* Анализ эффективности работы сайта и его улучшение.
* Заключение и исполнение договоров, стороной которых является субъект ПДн.
**IV. Перечень обрабатываемых ПДн**
4.1. В рамках исполнения целей, указанных в п. 3.2., Оператор может обрабатывать следующие категории ПДн, предоставляемые пользователем добровольно:
* Имя (предоставленное пользователем);
* Номер телефона (для оперативной связи).
* Электронная почта
4.2. **Обезличенные данные:** Оператор также обрабатывает обезличенные данные, которые не используются для идентификации конкретного пользователя: IP-адрес, данные о браузере, информация, содержащаяся в файлах cookie, сведения о действиях на сайте. Обработка обезличенных данных осуществляется исключительно для ведения статистики и улучшения работы сайта.
**V. Порядок и условия обработки ПДн**
5.1. **Согласие субъекта:** Обработка ПДн осуществляется с согласия субъекта ПДн. Такое согласие считается полученным в момент, когда пользователь инициирует контакт с Оператором через предоставленные каналы связи, тем самым добровольно предоставляя свои данные для обратной связи.
5.2. **Локализация данных:** В соответствии с ч. 5 ст. 18 ФЗ № 152, Оператор обеспечивает запись, систематизацию, накопление, хранение, уточнение (обновление, изменение) и извлечение ПДн граждан Российской Федерации с использованием баз данных, находящихся на территории Российской Федерации.
5.3. **Срок обработки:** Обработка ПДн осуществляется с момента получения согласия и прекращается:
* после достижения целей обработки;
* в случае отзыва согласия субъекта ПДн;
* при ликвидации Оператора.
5.4. **Передача третьим лицам:** Оператор не раскрывает и не передает ПДн третьим лицам без согласия субъекта ПДн, за исключением случаев, предусмотренных законодательством РФ.
**VI. Права субъекта персональных данных**
Субъект ПДн имеет право:
* Получить информацию, касающуюся обработки его ПДн.
* Требовать уточнения своих ПДн, их блокирования или уничтожения в случае, если данные являются неполными, устаревшими, неточными, незаконно полученными или не являются необходимыми для заявленной цели обработки.
* Отозвать свое согласие на обработку ПДн путем направления соответствующего запроса Оператору.
**VII. Заключительные положения**
7.1. Настоящая Политика вступает в силу с момента ее утверждения Оператором и является общедоступной.
7.2. Настоящая Политика подлежит размещению на официальном сайте Оператора.
7.3. Контроль за выполнением требований настоящей Политики осуществляется Оператором.
`;
const DATA_STORAGE_POLICY = `
**ПОЛИТИКА ХРАНЕНИЯ И ЗАЩИТЫ ДАННЫХ ООО «ИЗИ ГРУПП»**
**1. Общие гарантии**
1.1. ООО «ИЗИ ГРУПП» (далее — Оператор) гарантирует полную конфиденциальность и безопасность данных клиентов и пользователей в строгом соответствии с законодательством Российской Федерации, включая ФЗ № 152 «О персональных данных».
1.2. Приоритетом Оператора является предотвращение несанкционированного доступа, уничтожения, изменения, блокирования, копирования, распространения, а также иных неправомерных действий третьих лиц в отношении хранимой информации.
**2. Места и способы хранения**
2.1. **Локализация:** Все данные граждан Российской Федерации хранятся исключительно на серверах, физически расположенных на территории Российской Федерации.
2.2. **Электронные носители:** Хранение данных осуществляется в защищенных электронных базах данных. Доступ к серверам ограничен и защищен современными программно-аппаратными средствами.
2.3. **Бумажные носители:** В случае использования бумажных носителей, они хранятся в запираемых шкафах или сейфах, доступ к которым имеют только уполномоченные сотрудники.
**3. Меры по обеспечению безопасности**
Для защиты данных Оператор применяет следующий комплекс мер:
* **Технические меры:** Использование антивирусного программного обеспечения, межсетевых экранов (firewalls), средств шифрования данных при их передаче по каналам связи (протоколы SSL/TLS).
* **Организационные меры:** Регулярное обучение сотрудников правилам информационной безопасности, назначение ответственных лиц за организацию обработки данных.
* **Управление доступом:** Доступ к персональным данным предоставляется только тем сотрудникам, которым он необходим для выполнения должностных обязанностей. Каждому сотруднику присваивается уникальный идентификатор (логин) и пароль.
**4. Конфиденциальность**
4.1. Оператор обязуется не раскрывать третьим лицам и не распространять персональные данные без согласия субъекта, если иное не предусмотрено федеральным законом.
4.2. Все сотрудники Оператора, имеющие доступ к данным, подписывают обязательство о неразглашении конфиденциальной информации.
**5. Уничтожение данных**
5.1. По достижении целей обработки или в случае утраты необходимости в достижении этих целей, если иное не предусмотрено федеральным законом, данные подлежат уничтожению.
5.2. Уничтожение производится способом, исключающим возможность восстановления содержания данных (например, физическое уничтожение носителя или безвозвратное удаление файлов с использованием специализированного ПО).
`;
const Team: React.FC<SectionProps> = ({ onOpenModal }) => {
const handleMemberClick = (member: Member) => {
if (onOpenModal) {
onOpenModal({
title: member.name,
type: 'article',
theme: 'dark',
content: {
text: `**${member.role}**\n\n${member.bio}\n\nКлючевые компетенции:\n- Стратегическое планирование\n- Управление R&D\n- Взаимодействие с госсектором`
}
})
}
}
const handleLaunchPilot = () => {
if (onOpenModal) {
onOpenModal({
title: 'Запуск пилотного проекта',
type: 'form',
theme: 'dark',
content: {
text: 'Оставьте заявку на развертывание пилотной версии системы в вашем регионе или ведомстве.'
}
})
}
}
const handleLegal = (title: string) => {
if (onOpenModal) {
let content = '';
if (title === 'Политика конфиденциальности') {
content = PRIVACY_POLICY;
} else if (title === 'Политика хранения данных') {
content = DATA_STORAGE_POLICY;
}
onOpenModal({
title: title,
type: 'article',
theme: 'dark',
content: {
text: content
}
})
}
}
return (
<SectionWrapper id="team" transitionEffect="gradient" fitScreen={false}>
<div className="max-w-7xl mx-auto px-6 md:px-12 w-full flex flex-col justify-between items-center text-center relative min-h-full pb-20 lg:pb-10">
<div className="w-full flex flex-col items-center flex-grow justify-center py-12 lg:py-6">
<motion.div
className="inline-block border border-theme px-4 py-1 rounded-full mb-6 lg:mb-4 font-mono text-xs text-theme-muted"
initial={{ opacity: 0, y: -20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
iiEasy Group
</motion.div>
<motion.h2
className="text-4xl md:text-6xl lg:text-7xl font-black text-theme-main mb-6 lg:mb-4 tracking-tighter"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, ease: [0.645, 0.045, 0.355, 1.000] }}
>
ЯДРО КОМАНДЫ
</motion.h2>
<motion.p
className="text-base md:text-lg text-theme-muted mb-8 lg:mb-8 max-w-2xl mx-auto"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ delay: 0.3, duration: 0.8 }}
>
Локальная команда разработчиков, интегрированная в специфику регионального управления.
</motion.p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 lg:gap-4 mb-10 lg:mb-12 w-full max-w-6xl px-4 md:px-0">
{members.map((member, i) => (
<motion.div
key={member.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
whileTap={{ scale: 0.95 }}
transition={{ delay: 0.4 + i * 0.1, duration: 0.6 }}
onClick={() => handleMemberClick(member)}
className="bg-theme-card p-4 lg:p-5 border border-theme hover:border-[#20e3b2] transition-all duration-300 cursor-pointer group flex flex-col items-center hover:-translate-y-2 rounded-sm"
>
<div className="w-20 h-20 md:w-28 md:h-28 lg:w-20 lg:h-20 rounded-full mb-4 overflow-hidden border-2 border-transparent group-hover:border-[#20e3b2] transition-all duration-500">
{/* Use responsive classes: grayscale-0 on mobile, grayscale on large screens. */}
<img
src={member.image}
alt={member.name}
className="w-full h-full object-cover grayscale-0 lg:grayscale lg:group-hover:grayscale-0 transition-all duration-500"
/>
</div>
<h3 className="text-theme-main font-bold text-base lg:text-sm mb-1 font-mono">{member.name}</h3>
<p className="text-theme-muted text-[10px] uppercase tracking-wider mb-2 h-6 flex items-center justify-center leading-tight">{member.role}</p>
<div className="w-full h-px bg-theme-main/10 group-hover:bg-[#20e3b2]/30 transition-colors my-2"></div>
{/* Always visible on mobile, hidden and revealed on hover on desktop */}
<div className="text-[10px] text-theme-muted/60 opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity">
Нажмите для инфо
</div>
</motion.div>
))}
</div>
<motion.button
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
whileTap={{ scale: 0.95 }}
transition={{ delay: 0.8 }}
onClick={handleLaunchPilot}
className="px-8 py-4 lg:px-10 lg:py-4 bg-theme-main text-theme-card font-bold font-mono text-base lg:text-lg hover:bg-[#20e3b2] hover:text-black transition-colors duration-300 flex items-center gap-4 mx-auto border border-theme-main mb-8 lg:mb-8"
>
ЗАПУСТИТЬ ПИЛОТ
<ArrowRight className="w-5 h-5" />
</motion.button>
</div>
{/* Enhanced Footer */}
<footer className="w-full py-6 lg:py-4 border-t border-theme/20 bg-theme-card/30 backdrop-blur-sm mt-auto relative z-20">
<div className="max-w-7xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-4 lg:gap-3 text-[10px] lg:text-xs font-mono text-theme-muted uppercase tracking-wider text-center md:text-left">
<div className="flex flex-col md:flex-row gap-2 md:gap-6 items-center">
<span className="font-bold text-theme-main text-xs">ООО «ИЗИ ГРУПП»</span>
<a href="https://иилегко.рф" className="hover:text-[#20e3b2] transition-colors border-b border-transparent hover:border-[#20e3b2] pb-0.5">
иилегко.рф
</a>
</div>
<div className="flex flex-col md:flex-row gap-2 md:gap-6 items-center">
<button onClick={() => handleLegal('Политика конфиденциальности')} className="hover:text-theme-main transition-colors opacity-70 hover:opacity-100">
Политика конфиденциальности
</button>
<button onClick={() => handleLegal('Политика хранения данных')} className="hover:text-theme-main transition-colors opacity-70 hover:opacity-100">
Политика хранения данных
</button>
</div>
</div>
</footer>
</div>
</SectionWrapper>
);
};
export default Team;

166
components/TechVisualizer.tsx Executable file
View File

@@ -0,0 +1,166 @@
import React, { useRef, useMemo, useEffect } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
import * as THREE from 'three';
// Add global declaration to fix TypeScript errors with React Three Fiber elements
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
points: any;
bufferGeometry: any;
bufferAttribute: any;
pointsMaterial: any;
ambientLight: any;
}
}
}
declare global {
namespace JSX {
interface IntrinsicElements {
points: any;
bufferGeometry: any;
bufferAttribute: any;
pointsMaterial: any;
ambientLight: any;
}
}
}
interface TechVisualizerProps {
mode: 'server' | 'monitor' | 'hidden';
}
const ParticleMorph = ({ mode }: { mode: 'server' | 'monitor' | 'hidden' }) => {
const meshRef = useRef<THREE.Points>(null);
const COUNT = 3000;
// Define geometries
const data = useMemo(() => {
const pos = new Float32Array(COUNT * 3);
const col = new Float32Array(COUNT * 3);
const serverPos = new Float32Array(COUNT * 3);
const monitorPos = new Float32Array(COUNT * 3);
// SERVER SHAPE (Rectangular Tower)
for (let i = 0; i < COUNT; i++) {
const x = (Math.random() - 0.5) * 2; // Thin width
const y = (Math.random() - 0.5) * 6; // Tall
const z = (Math.random() - 0.5) * 2; // Depth
serverPos[i * 3] = x;
serverPos[i * 3 + 1] = y;
serverPos[i * 3 + 2] = z;
// Add some "layers" to make it look like a rack
if (Math.random() > 0.8) {
serverPos[i * 3] *= 1.2;
serverPos[i * 3 + 2] *= 1.2;
}
}
// MONITOR SHAPE (Curved Plane)
for (let i = 0; i < COUNT; i++) {
const x = (Math.random() - 0.5) * 8; // Wide
const y = (Math.random() - 0.5) * 3.5; // Aspect ratio
// Curve the Z based on X (parabolic)
const z = Math.pow(x * 0.3, 2) - 2;
monitorPos[i * 3] = x;
monitorPos[i * 3 + 1] = y + 0.5; // Lift up slightly
monitorPos[i * 3 + 2] = z;
// Bezel/Frame particles
if (Math.random() > 0.95) {
monitorPos[i * 3 + 2] += 0.1;
}
}
// Initial Positions (start at server)
for (let i = 0; i < COUNT * 3; i++) {
pos[i] = serverPos[i];
col[i] = 1; // Initial white/cyan mix logic in shader or simple color
}
return { positions: pos, colors: col, serverPos, monitorPos };
}, []);
// Buffer attributes
const bufferRef = useRef<THREE.BufferAttribute>(null);
useFrame((state, delta) => {
if (!meshRef.current || !bufferRef.current) return;
const target = mode === 'monitor' ? data.monitorPos : data.serverPos;
const current = bufferRef.current.array as Float32Array;
// Morph speed
const speed = 4 * delta;
// Visibility transition
const isHidden = mode === 'hidden';
for (let i = 0; i < COUNT; i++) {
const ix = i * 3;
const iy = i * 3 + 1;
const iz = i * 3 + 2;
if (isHidden) {
// Explode/Hide
current[ix] = THREE.MathUtils.lerp(current[ix], current[ix] * 1.01, speed);
current[iy] = THREE.MathUtils.lerp(current[iy], current[iy] + 10, speed);
} else {
// Standard Morph
current[ix] = THREE.MathUtils.lerp(current[ix], target[ix], speed);
current[iy] = THREE.MathUtils.lerp(current[iy], target[iy], speed);
current[iz] = THREE.MathUtils.lerp(current[iz], target[iz], speed);
// Add subtle noise/floating
current[iy] += Math.sin(state.clock.elapsedTime + current[ix]) * 0.002;
}
}
bufferRef.current.needsUpdate = true;
// Rotate entire mesh slowly
meshRef.current.rotation.y += delta * 0.1;
});
return (
<points ref={meshRef}>
<bufferGeometry>
<bufferAttribute
ref={bufferRef}
attach="attributes-position"
array={data.positions}
count={data.positions.length / 3}
itemSize={3}
/>
</bufferGeometry>
<pointsMaterial
size={0.04}
color={mode === 'monitor' ? "#ff4b4b" : "#20e3b2"} // Red for monitor (Anime.js accent), Cyan for Server
transparent
opacity={0.8}
sizeAttenuation
blending={THREE.AdditiveBlending}
/>
</points>
);
};
const TechVisualizer: React.FC<TechVisualizerProps> = ({ mode }) => {
return (
<div className={`fixed inset-0 z-[1] transition-opacity duration-700 pointer-events-none ${mode === 'hidden' ? 'opacity-0' : 'opacity-60'}`}>
<Canvas gl={{ antialias: true, alpha: true }}>
<PerspectiveCamera makeDefault position={[0, 0, 8]} fov={50} />
<ambientLight intensity={0.5} />
<ParticleMorph mode={mode} />
</Canvas>
</div>
);
};
export default TechVisualizer;

92
components/Timeline.tsx Executable file
View File

@@ -0,0 +1,92 @@
import React, { useState, useEffect, useRef } from 'react';
const sections = [
{ id: 'hero', label: 'Старт' },
{ id: 'infra', label: 'ЦОД' },
{ id: 'science', label: 'Наука' },
{ id: 'social', label: 'Социум' },
{ id: 'leadership', label: 'Рейтинг' },
{ id: 'metrics', label: 'Цифры' },
{ id: 'scaling', label: 'Масштаб' },
{ id: 'team', label: 'Команда' },
];
const Timeline: React.FC = () => {
const [activeId, setActiveId] = useState<string>('hero');
const observerRef = useRef<IntersectionObserver | null>(null);
const visibilities = useRef<Map<string, number>>(new Map());
useEffect(() => {
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
// Calculate approximate visible height in pixels
// This creates a fair comparison between scrolling (tall) sections and snapping (short) sections
const visibleHeight = entry.intersectionRect.height;
visibilities.current.set(entry.target.id, visibleHeight);
});
let maxVisibleHeight = 0;
let maxId = '';
// The active section is the one occupying the most vertical space in the viewport
for (const [id, height] of visibilities.current.entries()) {
if (height > maxVisibleHeight) {
maxVisibleHeight = height;
maxId = id;
}
}
if (maxId && maxVisibleHeight > 0) {
setActiveId(maxId);
}
};
const options = {
root: null, // viewport
rootMargin: '0px',
threshold: Array.from({ length: 11 }, (_, i) => i * 0.1),
};
observerRef.current = new IntersectionObserver(handleIntersection, options);
// Observe all sections
sections.forEach(({ id }) => {
const element = document.getElementById(id);
if (element) observerRef.current?.observe(element);
});
return () => observerRef.current?.disconnect();
}, []);
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
e.preventDefault();
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<div className="fixed right-8 top-1/2 -translate-y-1/2 z-[60] hidden md:flex flex-col gap-3 mix-blend-difference items-end">
{sections.map((item) => {
const isActive = activeId === item.id;
return (
<a
key={item.id}
href={`#${item.id}`}
onClick={(e) => handleClick(e, item.id)}
className="group flex flex-row-reverse items-center justify-end gap-3"
>
<div className={`w-1.5 h-1.5 transition-all duration-300 ${isActive ? 'bg-[#ff4b4b] scale-150' : 'bg-white/30 group-hover:bg-white'}`} />
<span className={`text-[10px] font-mono transition-all duration-300 uppercase tracking-widest text-white ${isActive ? 'opacity-100' : 'opacity-0 translate-x-2 group-hover:opacity-50 group-hover:translate-x-0'}`}>
{item.label}
</span>
</a>
);
})}
</div>
);
};
export default Timeline;

201
components/VisualEffects.tsx Executable file
View File

@@ -0,0 +1,201 @@
import React, { useRef, useMemo, useEffect } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import * as THREE from 'three';
// Add global declaration to fix TypeScript errors with React Three Fiber elements
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
points: any;
bufferGeometry: any;
bufferAttribute: any;
pointsMaterial: any;
ambientLight: any;
}
}
}
declare global {
namespace JSX {
interface IntrinsicElements {
points: any;
bufferGeometry: any;
bufferAttribute: any;
pointsMaterial: any;
ambientLight: any;
}
}
}
// --- WARP EFFECT ---
const WarpParticles = () => {
const count = 2000;
const mesh = useRef<THREE.Points>(null);
// Particles setup
const particles = useMemo(() => {
const temp = [];
for (let i = 0; i < count; i++) {
const x = (Math.random() - 0.5) * 30;
const y = (Math.random() - 0.5) * 60; // Taller spread
const z = (Math.random() - 0.5) * 20;
temp.push(x, y, z);
}
return new Float32Array(temp);
}, [count]);
useFrame((state, delta) => {
if (!mesh.current) return;
const time = state.clock.getElapsedTime();
// Logic:
// 1. Initial burst (0-1s): Speed = 3.0
// 2. Slow down (1s+): Speed = 0.5 (2x slower than a standard '1.0' baseline)
let targetSpeed = 0.5; // Slow cruising speed
if (time < 1.0) {
targetSpeed = 4.0; // Fast entry
} else if (time < 1.8) {
// Smooth deceleration phase
const t = (time - 1.0) / 0.8;
targetSpeed = THREE.MathUtils.lerp(4.0, 0.5, t);
}
const positions = mesh.current.geometry.attributes.position.array as Float32Array;
const moveY = targetSpeed * delta * 10; // Scale speed to movement
for (let i = 0; i < count; i++) {
// Move particles up
positions[i * 3 + 1] += moveY;
// Seamless wrap around
if (positions[i * 3 + 1] > 30) {
positions[i * 3 + 1] -= 60; // Subtract height to wrap seamlessly
// Randomize X/Z on respawn to create new star patterns
positions[i * 3] = (Math.random() - 0.5) * 30;
positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
}
}
mesh.current.geometry.attributes.position.needsUpdate = true;
// Stretch effect based on speed
// Higher speed = more vertical stretch
mesh.current.scale.y = 1 + targetSpeed * 0.5;
});
return (
<points ref={mesh}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={particles.length / 3}
array={particles}
itemSize={3}
/>
</bufferGeometry>
<pointsMaterial
size={0.08}
color="#ffffff"
transparent
opacity={0.6}
sizeAttenuation
blending={THREE.AdditiveBlending}
depthWrite={false}
/>
</points>
);
};
export const WarpOverlay = () => {
return (
<div className="absolute inset-0 z-40 pointer-events-none mix-blend-screen">
<Canvas
camera={{ position: [0, 0, 15], fov: 60 }}
gl={{ alpha: true }}
style={{ pointerEvents: 'none' }} // Explicitly disable pointer events on canvas element
>
<WarpParticles />
</Canvas>
</div>
);
};
// --- SAND EFFECT ---
const SandParticles = () => {
const count = 4000;
const mesh = useRef<THREE.Points>(null);
const particles = useMemo(() => {
const temp = [];
for (let i = 0; i < count; i++) {
const x = (Math.random() - 0.5) * 25;
const y = (Math.random() - 0.5) * 25;
const z = (Math.random() - 0.5) * 10;
temp.push(x, y, z);
}
return new Float32Array(temp);
}, [count]);
useFrame((state) => {
if (!mesh.current) return;
const positions = mesh.current.geometry.attributes.position.array as Float32Array;
const time = state.clock.getElapsedTime();
for (let i = 0; i < count; i++) {
const ix = i * 3;
const iy = i * 3 + 1;
const iz = i * 3 + 2;
// Chaotic wind movement
positions[ix] += Math.sin(time * 0.5 + positions[iy] * 0.5) * 0.02;
positions[iy] += Math.cos(time * 0.3 + positions[ix] * 0.5) * 0.01;
// Wrap around
if (Math.abs(positions[ix]) > 12) positions[ix] *= -0.9;
if (Math.abs(positions[iy]) > 12) positions[iy] *= -0.9;
}
mesh.current.geometry.attributes.position.needsUpdate = true;
mesh.current.rotation.y = time * 0.05;
});
return (
<points ref={mesh}>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
count={particles.length / 3}
array={particles}
itemSize={3}
/>
</bufferGeometry>
<pointsMaterial
size={0.05}
color="#c4c3be"
transparent
opacity={0.8}
sizeAttenuation
depthWrite={false}
/>
</points>
);
};
export const SandOverlay = () => {
return (
<div className="absolute inset-0 z-40 pointer-events-none">
<Canvas
camera={{ position: [0, 0, 10], fov: 60 }}
gl={{ alpha: true }}
style={{ pointerEvents: 'none' }} // Explicitly disable pointer events on canvas element
>
<ambientLight intensity={1} />
<SandParticles />
</Canvas>
</div>
);
};

25
ecosystem.config.cjs Executable file
View File

@@ -0,0 +1,25 @@
module.exports = {
apps: [
{
name: 'gov-back',
script: './backend-server/index.js',
cwd: './',
env: {
NODE_ENV: 'production',
PORT: 3001
},
watch: false
},
{
name: 'gov-front',
script: 'npm',
args: 'run dev',
cwd: './',
watch: false,
env: {
NODE_ENV: 'development'
}
}
]
};

BIN
gov-llm-site.tar Normal file

Binary file not shown.

119
index.html Executable file
View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="ru" class="h-full">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Суверенная LLM - Башкортостан</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='100' cy='100' r='70' stroke='%23ff4b4b' stroke-width='6' fill='none' stroke-dasharray='15 85'%3E%3C/circle%3E%3Ccircle cx='100' cy='100' r='50' stroke='%23ff4b4b' stroke-width='6' fill='none' stroke-dasharray='12 58' transform='rotate(30 100 100)'%3E%3C/circle%3E%3Ccircle cx='100' cy='100' r='30' stroke='%23ff4b4b' stroke-width='6' fill='none' stroke-dasharray='8 32' transform='rotate(60 100 100)'%3E%3C/circle%3E%3C/svg%3E" />
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;900&family=JetBrains+Mono:wght@400;700&display=swap');
:root {
/* Default Dark Theme (Anime.js style) */
--bg-color: #0e0e10;
--card-bg: #161618;
--text-main: #ffffff;
--text-muted: #888899;
--border-color: #333333;
--accent-red: #ff4b4b;
--accent-cyan: #20e3b2;
--grid-color: #222;
}
/* Light / Sand Theme */
body.theme-light {
--bg-color: #e3e1db; /* Slightly darker sand for contrast */
--card-bg: #ebe9e4;
--text-main: #1a1a1a;
--text-muted: #5c5c55;
--border-color: #c4c3be;
--accent-red: #d13030;
--accent-cyan: #008f72;
--grid-color: #d1d0ca;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
overflow: hidden; /* Prevent body scroll, handle in App container */
height: 100%;
transition: background-color 0.8s ease, color 0.8s ease;
}
.font-mono {
font-family: 'JetBrains Mono', monospace;
}
/* Utilities using CSS variables */
.bg-theme-main { background-color: var(--bg-color); transition: background-color 0.8s ease; }
.bg-theme-card { background-color: var(--card-bg); transition: background-color 0.8s ease; }
.text-theme-main { color: var(--text-main); transition: color 0.8s ease; }
.text-theme-muted { color: var(--text-muted); transition: color 0.8s ease; }
.border-theme { border-color: var(--border-color); transition: border-color 0.8s ease; }
/* Custom scrollbar for the container */
.snap-container::-webkit-scrollbar {
width: 6px;
}
.snap-container::-webkit-scrollbar-track {
background: var(--bg-color);
}
.snap-container::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.snap-container::-webkit-scrollbar-thumb:hover {
background: var(--accent-red);
}
::selection {
background: var(--accent-red);
color: white;
}
/* Grid Background Pattern */
.grid-pattern {
background-image: linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 40px 40px;
opacity: 0.3;
pointer-events: none;
}
</style>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.3.1",
"react/": "https://esm.sh/react@18.3.1/",
"react-dom": "https://esm.sh/react-dom@18.3.1",
"react-dom/": "https://esm.sh/react-dom@18.3.1/",
"react-dom/client": "https://esm.sh/react-dom@18.3.1/client",
"recharts": "https://esm.sh/recharts@2.12.7?external=react,react-dom",
"lucide-react": "https://esm.sh/lucide-react@0.395.0?external=react",
"framer-motion": "https://esm.sh/framer-motion@11.2.10?external=react,react-dom",
"three": "https://esm.sh/three@0.165.0",
"@react-three/fiber": "https://esm.sh/@react-three/fiber@8.16.8?external=react,react-dom,three",
"@react-three/drei": "https://esm.sh/@react-three/drei@9.108.3?external=react,react-dom,three,@react-three/fiber"
}
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body class="h-full">
<div id="root" class="h-full"></div>
<!-- Filters for visual effects -->
<svg style="position: absolute; width: 0; height: 0;">
<defs>
<filter id="sand-dissolve">
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="3" result="noise" />
<feDisplacementMap in="SourceGraphic" in2="noise" scale="0" xChannelSelector="R" yChannelSelector="G" />
</filter>
</defs>
</svg>
<script type="module" src="/index.tsx"></script>
</body>
</html>

15
index.tsx Executable file
View File

@@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

BIN
media/2.mp4 Executable file

Binary file not shown.

BIN
media/block2.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

20
media/datakeep.txt Executable file
View File

@@ -0,0 +1,20 @@
Политика хранения и защиты данных
Этот документ дополняет основную политику и детализирует ваши гарантии безопасности.
ПОЛИТИКА ХРАНЕНИЯ И ЗАЩИТЫ ДАННЫХ ООО «ИЗИ ГРУПП»
1. Общие гарантии 1.1. ООО «ИЗИ ГРУПП» (далее — Оператор) гарантирует полную конфиденциальность и безопасность данных клиентов и пользователей в строгом соответствии с законодательством Российской Федерации, включая ФЗ № 152 «О персональных данных». 1.2. Приоритетом Оператора является предотвращение несанкционированного доступа, уничтожения, изменения, блокирования, копирования, распространения, а также иных неправомерных действий третьих лиц в отношении хранимой информации.
2. Места и способы хранения 2.1. Локализация: Все данные граждан Российской Федерации хранятся исключительно на серверах, физически расположенных на территории Российской Федерации. 2.2. Электронные носители: Хранение данных осуществляется в защищенных электронных базах данных. Доступ к серверам ограничен и защищен современными программно-аппаратными средствами. 2.3. Бумажные носители: В случае использования бумажных носителей, они хранятся в запираемых шкафах или сейфах, доступ к которым имеют только уполномоченные сотрудники.
3. Меры по обеспечению безопасности Для защиты данных Оператор применяет следующий комплекс мер:
Технические меры: Использование антивирусного программного обеспечения, межсетевых экранов (firewalls), средств шифрования данных при их передаче по каналам связи (протоколы SSL/TLS).
Организационные меры: Регулярное обучение сотрудников правилам информационной безопасности, назначение ответственных лиц за организацию обработки данных.
Управление доступом: Доступ к персональным данным предоставляется только тем сотрудникам, которым он необходим для выполнения должностных обязанностей. Каждому сотруднику присваивается уникальный идентификатор (логин) и пароль.
4. Конфиденциальность 4.1. Оператор обязуется не раскрывать третьим лицам и не распространять персональные данные без согласия субъекта, если иное не предусмотрено федеральным законом. 4.2. Все сотрудники Оператора, имеющие доступ к данным, подписывают обязательство о неразглашении конфиденциальной информации.
5. Уничтожение данных 5.1. По достижении целей обработки или в случае утраты необходимости в достижении этих целей, если иное не предусмотрено федеральным законом, данные подлежат уничтожению. 5.2. Уничтожение производится способом, исключающим возможность восстановления содержания данных (например, физическое уничтожение носителя или безвозвратное удаление файлов с использованием специализированного ПО).

66
media/polkonf.txt Executable file
View File

@@ -0,0 +1,66 @@
1. Политика обработки персональных данных
ПОЛИТИКА ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ ООО «ИЗИ ГРУПП»
I. Общие положения 1.1. Настоящая Политика обработки персональных данных (далее Политика) разработана в соответствии с требованиями Федерального закона от 27.07.2006 № 152-ФЗ «О персональных данных» (далее ФЗ № 152) и определяет порядок обработки персональных данных (ПДн) и меры по обеспечению безопасности ПДн в Обществе с ограниченной ответственностью «ИЗИ ГРУПП» (далее Оператор). 1.2. Оператор осуществляет обработку ПДн с целью обеспечения соблюдения законов РФ, а также с целью осуществления своей основной деятельности: предоставление информации о продуктах и услугах, консультирование, ответ на обращения пользователей, а также заключение и исполнение договоров. 1.3. Действие Политики распространяется на все процессы Оператора, связанные с обработкой ПДн.
II. Сведения об Операторе
Наименование: Общество с ограниченной ответственностью «ИЗИ ГРУПП» (ООО «ИЗИ ГРУПП»)
Адрес: [Укажите юридический адрес компании]
ИНН: [Укажите ИНН]
КПП: [Укажите КПП]
Сайт: [Укажите адрес сайта, например: easy-group.ru]
III. Принципы и цели обработки ПДн 3.1. Принципы обработки ПДн: Обработка ПДн осуществляется на законной и справедливой основе, ограничивается достижением конкретных, заранее определенных и законных целей. Содержание и объем обрабатываемых ПДн соответствуют заявленным целям обработки.
3.2. Цели обработки ПДн:
Установление обратной связи с пользователем по его запросу, инициированному через предоставленные каналы связи (в том числе через мессенджеры).
Предоставление информации о продуктах, экспертизе и услугах Оператора.
Анализ эффективности работы сайта и его улучшение.
Заключение и исполнение договоров, стороной которых является субъект ПДн.
IV. Перечень обрабатываемых ПДн 4.1. В рамках исполнения целей, указанных в п. 3.2., Оператор может обрабатывать следующие категории ПДн, предоставляемые пользователем добровольно:
Имя (предоставленное пользователем);
Номер телефона (для оперативной связи).
4.2. Обезличенные данные: Оператор также обрабатывает обезличенные данные, которые не используются для идентификации конкретного пользователя: IP-адрес, данные о браузере, информация, содержащаяся в файлах cookie, сведения о действиях на сайте. Обработка обезличенных данных осуществляется исключительно для ведения статистики и улучшения работы сайта.
V. Порядок и условия обработки ПДн 5.1. Согласие субъекта: Обработка ПДн осуществляется с согласия субъекта ПДн. Такое согласие считается полученным в момент, когда пользователь инициирует контакт с Оператором через предоставленные каналы связи, тем самым добровольно предоставляя свои данные для обратной связи. 5.2. Локализация данных: В соответствии с ч. 5 ст. 18 ФЗ № 152, Оператор обеспечивает запись, систематизацию, накопление, хранение, уточнение (обновление, изменение) и извлечение ПДн граждан Российской Федерации с использованием баз данных, находящихся на территории Российской Федерации. 5.3. Срок обработки: Обработка ПДн осуществляется с момента получения согласия и прекращается:
после достижения целей обработки;
в случае отзыва согласия субъекта ПДн;
при ликвидации Оператора. 5.4. Передача третьим лицам: Оператор не раскрывает и не передает ПДн третьим лицам без согласия субъекта ПДн, за исключением случаев, предусмотренных законодательством РФ.
VI. Права субъекта персональных данных Субъект ПДн имеет право:
Получить информацию, касающуюся обработки его ПДн.
Требовать уточнения своих ПДн, их блокирования или уничтожения в случае, если данные являются неполными, устаревшими, неточными, незаконно полученными или не являются необходимыми для заявленной цели обработки.
Отозвать свое согласие на обработку ПДн путем направления соответствующего запроса Оператору.
VII. Заключительные положения 7.1. Настоящая Политика вступает в силу с момента ее утверждения Оператором и является общедоступной. 7.2. Настоящая Политика подлежит размещению на официальном сайте Оператора. 7.3. Контроль за выполнением требований настоящей Политики осуществляется Оператором

83
media/rbsvg.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 89 KiB

BIN
media/team/alexandr.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
media/team/arsen.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

BIN
media/team/daniel.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

BIN
media/team/edgar.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

5
metadata.json Executable file
View File

@@ -0,0 +1,5 @@
{
"name": "Copy of Mob of Copy of Основной сайт для llm ",
"description": "Presentation landing page for the First Sovereign LLM System for Government Administration in Bashkortostan.",
"requestFramePermissions": []
}

3024
package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

27
package.json Executable file
View File

@@ -0,0 +1,27 @@
{
"name": "copy-of-mob-of-copy-of-основной-сайт-для-llm",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1",
"recharts": "2.12.7",
"lucide-react": "0.395.0",
"framer-motion": "11.2.10",
"three": "0.165.0",
"@react-three/fiber": "8.16.8",
"@react-three/drei": "9.108.3"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

29
tsconfig.json Executable file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

44
types.ts Executable file
View File

@@ -0,0 +1,44 @@
export interface SectionProps {
id?: string;
className?: string;
onOpenModal?: (data: ModalData) => void;
}
export interface MetricItem {
value: string;
label: string;
subtext: string;
}
export interface TeamMember {
name: string;
role: string;
image: string;
}
export type ModalType = 'article' | 'table' | 'list' | 'form';
export interface ModalData {
title: string;
type: ModalType;
theme: 'dark' | 'light';
content: {
text?: string;
headers?: string[];
rows?: string[][];
items?: string[];
};
}
declare global {
namespace JSX {
interface IntrinsicElements {
points: any;
bufferGeometry: any;
bufferAttribute: any;
pointsMaterial: any;
ambientLight: any;
}
}
}

34
vite.config.ts Executable file
View File

@@ -0,0 +1,34 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 4175,
host: true, // Позволяет Vite слушать все сетевые интерфейсы (0.0.0.0)
strictPort: true,
allowedHosts: ['gov.iieasy.ru'], // Разрешаем внешний хост
proxy: {
// Все запросы, начинающиеся с /api, будут перенаправлены на бэкенд
'/api': {
target: 'http://127.0.0.1:3001',
changeOrigin: true,
secure: false,
},
},
},
preview: {
port: 4175,
host: true, // Также для режима preview
strictPort: true,
allowedHosts: ['gov.iieasy.ru'], // Разрешаем внешний хост для preview
proxy: {
'/api': {
target: 'http://127.0.0.1:3001',
changeOrigin: true,
secure: false,
},
},
},
});