Initial commit gov-llm-v2
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
backend-server
|
||||||
24
.gitignore
vendored
Executable file
24
.gitignore
vendored
Executable 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
164
App.tsx
Executable 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
12
Dockerfile
Normal 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
20
README.md
Executable 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
8
backend-server/.env
Executable 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
82
backend-server/index.js
Executable 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
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
19
backend-server/package.json
Executable 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
72
components/BootAnimation.tsx
Executable 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
165
components/Hero.tsx
Executable 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
94
components/Infrastructure.tsx
Executable 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
145
components/Leadership.tsx
Executable 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
32
components/MapSVG.tsx
Executable 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
108
components/Metrics.tsx
Executable 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
447
components/Modal.tsx
Executable 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
130
components/Scaling.tsx
Executable 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]">>></span> Федеральный Масштаб
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-3">
|
||||||
|
<span className="text-[#ff4b4b]">>></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
117
components/Science.tsx
Executable 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">
|
||||||
|
> Соединение с базой данных... <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
207
components/SectionWrapper.tsx
Executable 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
121
components/Social.tsx
Executable 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
279
components/Team.tsx
Executable 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
166
components/TechVisualizer.tsx
Executable 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
92
components/Timeline.tsx
Executable 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
201
components/VisualEffects.tsx
Executable 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
25
ecosystem.config.cjs
Executable 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
BIN
gov-llm-site.tar
Normal file
Binary file not shown.
119
index.html
Executable file
119
index.html
Executable 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
15
index.tsx
Executable 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
BIN
media/2.mp4
Executable file
Binary file not shown.
BIN
media/block2.png
Executable file
BIN
media/block2.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
20
media/datakeep.txt
Executable file
20
media/datakeep.txt
Executable 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
66
media/polkonf.txt
Executable 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
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
BIN
media/team/alexandr.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
media/team/arsen.jpg
Executable file
BIN
media/team/arsen.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 468 KiB |
BIN
media/team/daniel.jpg
Executable file
BIN
media/team/daniel.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 700 KiB |
BIN
media/team/edgar.jpg
Executable file
BIN
media/team/edgar.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
5
metadata.json
Executable file
5
metadata.json
Executable 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
3024
package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
27
package.json
Executable file
27
package.json
Executable 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
29
tsconfig.json
Executable 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
44
types.ts
Executable 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
34
vite.config.ts
Executable 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user