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