Initial commit gov-llm-v2
This commit is contained in:
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user