Initial commit for iiEasy: all files included

This commit is contained in:
2026-02-03 23:16:16 +05:00
commit 3b3d29e21c
158 changed files with 32962 additions and 0 deletions

24
-1.gitignore Executable file
View File

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

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
node_modules
.git
iiEasy
.gitignore
.env
.env.local
.env.*.local
*.log
dist
.idea
.vscode
*.md
!README.md

10
.env.nomad.example Normal file
View File

@@ -0,0 +1,10 @@
# Copy to .env.nomad and source before deploy: source .env.nomad
# Do NOT commit .env.nomad (contains credentials)
export NOMAD_ADDR=http://192.168.1.16:4646
export NOMAD_HOST=192.168.1.16
export NOMAD_USER=its
export STRAPI_PUBLIC_URL=http://192.168.1.16:1337
# For SSH: use key-based auth or enter password when prompted
# ssh-copy-id its@192.168.1.16

25
.gitignore vendored Executable file
View File

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

497
App.tsx Executable file
View File

@@ -0,0 +1,497 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import Sidebar from './components/Sidebar';
import HeroSection from './components/HeroSection';
import BlogSection from './components/BlogSection';
import NewsSection from './components/NewsSection';
import ResearchSection from './components/ResearchSection';
import BusinessSection from './components/BusinessSection';
import ConnectSection from './components/ConnectSection';
import AboutUsSection from './components/AboutUsSection';
import OurMissionSection from './components/OurMissionSection';
import CareersSection from './components/CareersSection';
import BusinessLandingSection from './components/BusinessLandingSection';
import BusinessServicesSection from './components/BusinessServicesSection';
import EducationBusinessSection from './components/EducationBusinessSection';
import EducationStudentsSection from './components/EducationStudentsSection';
import ResearchAllSection from './components/ResearchAllSection';
import NewsAllSection from './components/NewsAllSection';
import StoriesAllSection from './components/StoriesAllSection';
import AcceleratorAboutSection from './components/AcceleratorAboutSection';
import AcceleratorProjectsSection from './components/AcceleratorProjectsSection';
import AcceleratorInvestmentSection from './components/AcceleratorInvestmentSection';
import TermsOfUseSection from './components/TermsOfUseSection';
import PrivacyPolicySection from './components/PrivacyPolicySection';
import Logo from './components/Logo';
import { Bars3Icon, XMarkIcon, SidebarToggleIcon } from './components/icons';
import { CurrentView, Post, NewsArticle, ResearchPaper, BusinessStory, ServiceItemData, ClientLogo, BusinessCourse, StudentProgram, AcceleratorProject, InvestmentProject, Vacancy } from './types';
import { SECTION_IDS, FOOTER_CONTENT, UI_TEXTS } from './constants';
import ChatInput from './components/ChatInput';
// Import Strapi service
import {
fetchPosts, fetchNewsArticles, fetchResearchPapers, fetchBusinessStories,
fetchServiceItems, fetchClientLogos, fetchBusinessCourses, fetchStudentPrograms,
fetchAcceleratorProjects, fetchInvestmentProjects, fetchVacancies
} from './strapiService';
// Import Detail Page Components
import BlogPostDetail from './components/BlogPostDetail';
import NewsArticleDetail from './components/NewsArticleDetail';
import ResearchPaperDetail from './components/ResearchPaperDetail';
import BusinessStoryDetail from './components/BusinessStoryDetail';
import VacancyDetail from './components/VacancyDetail';
const App: React.FC = () => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isDesktopSidebarCollapsed, setIsDesktopSidebarCollapsed] = useState(false);
const [currentView, setCurrentView] = useState<CurrentView>('main');
const [selectedItemId, setSelectedItemId] = useState<string | null>(null);
const [showStickyChat, setShowStickyChat] = useState(false);
const [isStickyChatExpanded, setIsStickyChatExpanded] = useState(false);
const stickyChatRef = useRef<HTMLDivElement>(null);
// State for Strapi data
const [posts, setPosts] = useState<Post[]>([]);
const [newsArticles, setNewsArticles] = useState<NewsArticle[]>([]);
const [researchPapers, setResearchPapers] = useState<ResearchPaper[]>([]);
const [businessStories, setBusinessStories] = useState<BusinessStory[]>([]);
const [serviceItems, setServiceItems] = useState<ServiceItemData[]>([]);
const [clientLogos, setClientLogos] = useState<ClientLogo[]>([]);
const [businessCourses, setBusinessCourses] = useState<BusinessCourse[]>([]);
const [studentPrograms, setStudentPrograms] = useState<StudentProgram[]>([]);
const [acceleratorProjects, setAcceleratorProjects] = useState<AcceleratorProject[]>([]);
const [investmentProjects, setInvestmentProjects] = useState<InvestmentProject[]>([]);
const [vacancies, setVacancies] = useState<Vacancy[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const toggleSidebar = useCallback(() => {
setIsSidebarOpen(prev => !prev);
}, []);
const handleSetCurrentView = useCallback((view: CurrentView) => {
setCurrentView(view);
if (!view.endsWith('Detail') && view !== 'termsOfUse' && view !== 'privacyPolicy') {
setSelectedItemId(null);
}
}, []);
const handleSetSelectedItemId = useCallback((id: string | null) => {
setSelectedItemId(id);
}, []);
useEffect(() => {
const loadData = async () => {
setIsLoading(true);
setError(null);
try {
const [
fetchedPosts,
fetchedNews,
fetchedResearch,
fetchedBusinessStories,
fetchedServiceItems,
fetchedClientLogos,
fetchedBusinessCourses,
fetchedStudentPrograms,
fetchedAcceleratorProjects,
fetchedInvestmentProjects,
fetchedVacanciesData
] = await Promise.all([
fetchPosts(),
fetchNewsArticles(),
fetchResearchPapers(),
fetchBusinessStories(),
fetchServiceItems(),
fetchClientLogos(),
fetchBusinessCourses(),
fetchStudentPrograms(),
fetchAcceleratorProjects(),
fetchInvestmentProjects(),
fetchVacancies(),
]);
setPosts(fetchedPosts);
setNewsArticles(fetchedNews);
setResearchPapers(fetchedResearch);
setBusinessStories(fetchedBusinessStories);
setServiceItems(fetchedServiceItems);
setClientLogos(fetchedClientLogos);
setBusinessCourses(fetchedBusinessCourses);
setStudentPrograms(fetchedStudentPrograms);
setAcceleratorProjects(fetchedAcceleratorProjects);
setInvestmentProjects(fetchedInvestmentProjects);
setVacancies(fetchedVacanciesData);
} catch (err) {
console.error("Failed to load data from Strapi:", err);
setError("Не удалось загрузить данные. Пожалуйста, попробуйте позже.");
} finally {
setIsLoading(false);
}
};
loadData();
}, []);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 768) {
setIsSidebarOpen(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
// On non-main pages, the sticky chat should always be visible.
if (currentView !== 'main') {
setShowStickyChat(true);
// We don't need a scroll listener on these pages for this feature.
return;
}
// On the main page, show the sticky chat only after scrolling past the hero.
const handleScroll = () => {
const heroSection = document.getElementById(SECTION_IDS.hero);
if (heroSection) {
const shouldShow = heroSection.getBoundingClientRect().bottom < 0;
setShowStickyChat(current => current === shouldShow ? current : shouldShow);
} else {
// Fallback if hero section is not found on main page for some reason
setShowStickyChat(false);
}
};
// Set initial state for 'main' view (in case user reloads scrolled down)
handleScroll();
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [currentView]); // Dependency on currentView ensures this logic re-runs on page change
// Hook for handling clicks outside of the sticky chat input to collapse it
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (isStickyChatExpanded && stickyChatRef.current && !stickyChatRef.current.contains(event.target as Node)) {
setIsStickyChatExpanded(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isStickyChatExpanded]);
useEffect(() => {
let targetId: string | null = null;
const baseTitle = 'iiEasy';
let newTitle = `iiEasy: Разработка Сайтов, Приложений, ИИ-Решений | ИИ Исследования и Обучение Уфа`; // Default
const findItemTitle = (items: {id: string; title: string}[], id: string | null) => items.find(item => item.id === id)?.title;
switch(currentView) {
case 'about': targetId = SECTION_IDS.aboutUsPage; newTitle = `О нас | ${baseTitle}`; break;
case 'mission': targetId = SECTION_IDS.ourMissionPage; newTitle = `Наша Миссия | ${baseTitle}`; break;
case 'careers': targetId = SECTION_IDS.careersPage; newTitle = `Карьера | ${baseTitle}`; break;
case 'businessLanding': targetId = SECTION_IDS.businessLandingPage; newTitle = `Кейсы для Бизнеса | ${baseTitle}`; break;
case 'businessServices': targetId = SECTION_IDS.businessServicesPage; newTitle = `Услуги для Бизнеса | ${baseTitle}`; break;
case 'educationBusiness': targetId = SECTION_IDS.educationBusinessPage; newTitle = `Обучение для Бизнеса | ${baseTitle}`; break;
case 'educationStudents': targetId = SECTION_IDS.educationStudentsPage; newTitle = `Обучение для Студентов | ${baseTitle}`; break;
case 'researchAll': targetId = SECTION_IDS.researchAllPage; newTitle = `Все Исследования | ${baseTitle}`; break;
case 'newsAll': targetId = SECTION_IDS.newsAllPage; newTitle = `Все Новости | ${baseTitle}`; break;
case 'storiesAll': targetId = SECTION_IDS.storiesAllPage; newTitle = `Все Истории | ${baseTitle}`; break;
case 'acceleratorAbout': targetId = SECTION_IDS.acceleratorAboutPage; newTitle = `Об Акселераторе | ${baseTitle}`; break;
case 'acceleratorProjects': targetId = SECTION_IDS.acceleratorProjectsPage; newTitle = `Проекты Акселератора | ${baseTitle}`; break;
case 'acceleratorInvestment': targetId = SECTION_IDS.acceleratorInvestmentPage; newTitle = `Инвестиции в стартапы | ${baseTitle}`; break;
case 'blogPostDetail':
targetId = SECTION_IDS.blogPostDetailPage;
newTitle = `${findItemTitle(posts, selectedItemId) || 'История'} | ${baseTitle}`;
break;
case 'newsArticleDetail':
targetId = SECTION_IDS.newsArticleDetailPage;
newTitle = `${findItemTitle(newsArticles, selectedItemId) || 'Новость'} | ${baseTitle}`;
break;
case 'researchPaperDetail':
targetId = SECTION_IDS.researchPaperDetailPage;
newTitle = `${findItemTitle(researchPapers, selectedItemId) || 'Исследование'} | ${baseTitle}`;
break;
case 'businessStoryDetail':
targetId = SECTION_IDS.businessStoryDetail;
newTitle = `${findItemTitle(businessStories, selectedItemId) || 'Кейс'} | ${baseTitle}`;
break;
case 'vacancyDetail':
targetId = SECTION_IDS.vacancyDetailPage;
newTitle = `${findItemTitle(vacancies, selectedItemId) || 'Вакансия'} | ${baseTitle}`;
break;
case 'termsOfUse': targetId = SECTION_IDS.termsOfUsePage; newTitle = `Правила использования | ${baseTitle}`; break;
case 'privacyPolicy': targetId = SECTION_IDS.privacyPolicyPage; newTitle = `Политика конфиденциальности | ${baseTitle}`; break;
case 'main':
window.scrollTo({ top: 0, behavior: 'smooth' });
break;
}
document.title = newTitle;
if (targetId) {
const element = document.getElementById(targetId);
if (element) {
element.scrollIntoView({behavior: 'smooth', block: 'start'});
} else {
setTimeout(() => {
document.getElementById(targetId)?.scrollIntoView({behavior: 'smooth', block: 'start'});
}, 150);
}
} else if (currentView !== 'main') {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}, [currentView, selectedItemId, posts, newsArticles, researchPapers, businessStories, vacancies]);
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="text-center">
<Logo width={150} height={112} />
<p className="mt-4 text-xl font-semibold text-slate-700 animate-pulse">Загрузка данных...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-white p-4">
<div className="text-center bg-white p-8 rounded-lg">
<Logo width={100} height={75} className="mx-auto mb-4 filter grayscale" />
<h2 className="text-2xl font-bold text-red-600 mb-3">Ошибка загрузки</h2>
<p className="text-slate-700 mb-6">{error}</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-2 bg-white text-red-600 border border-red-500 rounded-lg hover:bg-red-50 transition-colors font-semibold"
>
Попробовать снова
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen text-slate-800 font-inter bg-white">
<Sidebar
isOpen={isSidebarOpen}
onClose={() => setIsSidebarOpen(false)}
currentView={currentView}
setCurrentView={handleSetCurrentView}
isDesktopCollapsed={isDesktopSidebarCollapsed}
/>
{/* Desktop toggle button */}
<button
type="button"
onClick={() => setIsDesktopSidebarCollapsed(prev => !prev)}
className={`fixed top-6 left-4 z-50 p-2 bg-white border border-slate-200 text-slate-600 hover:text-black hover:bg-slate-100 rounded-lg shadow-sm transition-all duration-300 ease-in-out hidden md:block
${isDesktopSidebarCollapsed ? '' : 'md:translate-x-72'}`}
aria-label="Toggle navigation sidebar"
aria-expanded={!isDesktopSidebarCollapsed}
>
<SidebarToggleIcon className={`w-5 h-5 transition-transform duration-300 ${isDesktopSidebarCollapsed ? 'rotate-180' : ''}`} />
</button>
<div className={`transition-all duration-300 ease-in-out ${isDesktopSidebarCollapsed ? 'md:pl-0' : 'md:pl-72'}`}>
<div className="flex flex-col min-h-screen">
{isSidebarOpen && (
<div
className="fixed inset-0 z-30 bg-black/30 md:hidden"
onClick={toggleSidebar}
aria-hidden="true"
></div>
)}
<button
id="sidebar-toggle-button"
onClick={toggleSidebar}
className="fixed top-4 left-4 z-50 p-2 bg-white/80 rounded-full text-slate-800 hover:text-black focus:ring-slate-500 transition-all focus:outline-none focus:ring-2 md:hidden"
aria-label={isSidebarOpen ? UI_TEXTS.sidebarToggleCloseAriaLabel : UI_TEXTS.sidebarToggleOpenAriaLabel}
aria-expanded={isSidebarOpen}
>
{isSidebarOpen ? <XMarkIcon className="h-7 w-7" /> : <Bars3Icon className="h-7 w-7" />}
</button>
<main className="flex-grow">
{currentView === 'main' && (
<>
<HeroSection setCurrentView={handleSetCurrentView} isChatHidden={showStickyChat} />
<BlogSection posts={posts} setCurrentView={handleSetCurrentView} setSelectedItemId={handleSetSelectedItemId} />
<NewsSection newsArticles={newsArticles} setCurrentView={handleSetCurrentView} setSelectedItemId={handleSetSelectedItemId} />
<ResearchSection researchPapers={researchPapers} setCurrentView={handleSetCurrentView} setSelectedItemId={handleSetSelectedItemId} />
<BusinessSection businessStories={businessStories} setCurrentView={handleSetCurrentView} setSelectedItemId={handleSetSelectedItemId} />
<ConnectSection />
</>
)}
{currentView === 'about' && <AboutUsSection />}
{currentView === 'mission' && <OurMissionSection />}
{currentView === 'careers' && <CareersSection vacancies={vacancies} setCurrentView={handleSetCurrentView} setSelectedItemId={handleSetSelectedItemId} />}
{currentView === 'businessLanding' && <BusinessLandingSection businessStories={businessStories} setCurrentView={handleSetCurrentView} setSelectedItemId={handleSetSelectedItemId} />}
{currentView === 'businessServices' && <BusinessServicesSection serviceItems={serviceItems} clientLogos={clientLogos} />}
{currentView === 'educationBusiness' && <EducationBusinessSection businessCourses={businessCourses} />}
{currentView === 'educationStudents' && <EducationStudentsSection studentPrograms={studentPrograms} />}
{currentView === 'researchAll' && <ResearchAllSection researchPapers={researchPapers} setCurrentView={handleSetCurrentView} setSelectedItemId={handleSetSelectedItemId} />}
{currentView === 'newsAll' && <NewsAllSection newsArticles={newsArticles} setCurrentView={handleSetCurrentView} setSelectedItemId={handleSetSelectedItemId} />}
{currentView === 'storiesAll' && <StoriesAllSection posts={posts} setCurrentView={handleSetCurrentView} setSelectedItemId={handleSetSelectedItemId} />}
{currentView === 'acceleratorAbout' && <AcceleratorAboutSection />}
{currentView === 'acceleratorProjects' && <AcceleratorProjectsSection acceleratorProjects={acceleratorProjects} />}
{currentView === 'acceleratorInvestment' && <AcceleratorInvestmentSection investmentProjects={investmentProjects} />}
{/* Detail Pages */}
{currentView === 'blogPostDetail' && selectedItemId && (
<BlogPostDetail
itemId={selectedItemId}
allPosts={posts}
setCurrentView={handleSetCurrentView}
setSelectedItemId={handleSetSelectedItemId}
/>
)}
{currentView === 'newsArticleDetail' && selectedItemId && (
<NewsArticleDetail
itemId={selectedItemId}
allNews={newsArticles}
setCurrentView={handleSetCurrentView}
setSelectedItemId={handleSetSelectedItemId}
/>
)}
{currentView === 'researchPaperDetail' && selectedItemId && (
<ResearchPaperDetail
itemId={selectedItemId}
allResearchPapers={researchPapers}
setCurrentView={handleSetCurrentView}
setSelectedItemId={handleSetSelectedItemId}
/>
)}
{currentView === 'businessStoryDetail' && selectedItemId && (
<BusinessStoryDetail
itemId={selectedItemId}
allBusinessStories={businessStories}
setCurrentView={handleSetCurrentView}
setSelectedItemId={handleSetSelectedItemId}
/>
)}
{currentView === 'vacancyDetail' && selectedItemId && (
<VacancyDetail
itemId={selectedItemId}
allVacancies={vacancies}
setCurrentView={handleSetCurrentView}
setSelectedItemId={handleSetSelectedItemId}
/>
)}
{/* Legal Pages */}
{currentView === 'termsOfUse' && <TermsOfUseSection setCurrentView={handleSetCurrentView} />}
{currentView === 'privacyPolicy' && <PrivacyPolicySection setCurrentView={handleSetCurrentView} />}
</main>
<footer className="py-6 bg-white text-slate-600">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center md:text-left">
<div className="md:text-left">
<h3 className="text-sm font-semibold text-slate-500 mb-3 font-quicksand">{FOOTER_CONTENT.legalTitle}</h3>
<ul className="space-y-2 text-xs">
{FOOTER_CONTENT.legalLinks.map(link => (
<li key={link.href}>
<a
href={link.href}
onClick={(e) => {
e.preventDefault();
handleSetCurrentView(link.targetView);
}}
className="text-slate-600 hover:text-black transition-colors cursor-pointer"
>
{link.text}
</a>
</li>
))}
</ul>
</div>
<div className="flex flex-col items-center text-center">
<Logo width={60} height={60} className="mb-3" />
<p className="font-quicksand text-sm font-semibold text-slate-800">{FOOTER_CONTENT.copyrightText(new Date().getFullYear())}</p>
<p className="text-xs mt-2 text-slate-500">{FOOTER_CONTENT.tagline}</p>
</div>
<div className="md:text-right">
<h3 className="text-sm font-semibold text-slate-500 mb-3 font-quicksand">{FOOTER_CONTENT.socialTitle}</h3>
<div className="flex justify-center md:justify-end space-x-4">
{FOOTER_CONTENT.socialLinks.map(social => (
<a
key={social.name}
href={social.href}
target="_blank"
rel="noopener noreferrer"
aria-label={social.ariaLabel}
className="text-slate-600 hover:text-black transition-colors"
>
<social.icon className="w-5 h-5" />
</a>
))}
</div>
<div className="mt-6 text-xs">
<p>{FOOTER_CONTENT.address}</p>
<p className="mt-1">
<a href={FOOTER_CONTENT.emailHref} className="text-slate-600 hover:text-black transition-colors">
{FOOTER_CONTENT.email}
</a>
</p>
{FOOTER_CONTENT.companyName && FOOTER_CONTENT.companyInn && (
<p className="mt-1">
{FOOTER_CONTENT.companyName} <span className="text-slate-500">ИНН:</span> {FOOTER_CONTENT.companyInn}
</p>
)}
</div>
</div>
</div>
</div>
</footer>
</div>
</div>
{/* Sticky Chat Area */}
<div
ref={stickyChatRef}
className={`fixed bottom-0 left-0 right-0 z-20 transition-all duration-500 ease-in-out ${showStickyChat ? 'translate-y-0' : 'translate-y-full'}`}
>
<div className={`transition-all duration-300 ease-in-out ${isDesktopSidebarCollapsed ? 'md:pl-0' : 'md:pl-72'}`}>
{isStickyChatExpanded ? (
<div className="p-4 bg-gradient-to-t from-white via-white/95 to-transparent pointer-events-none">
<div className="w-full max-w-3xl mx-auto pointer-events-auto">
<ChatInput setCurrentView={setCurrentView} isSticky={true} autoFocusOnMount={true} />
</div>
</div>
) : (
<div className="w-full p-4 flex justify-center pointer-events-none">
<button
onClick={() => setIsStickyChatExpanded(true)}
className="pointer-events-auto bg-slate-100 text-slate-800 font-quicksand font-semibold px-6 py-3 rounded-full shadow-lg hover:bg-slate-200 transition-all duration-300 transform hover:-translate-y-1 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
aria-label="Открыть чат"
>
Поможем найти
</button>
</div>
)}
</div>
</div>
</div>
);
};
export default App;

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
# Этап 1: Сборка
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
RUN npm run build
# Этап 2: Раздача через Nginx
FROM nginx:stable-alpine
# 1. Копируем то, что собрал Vite (индекс, стили, основной JS)
COPY --from=build /app/dist /usr/share/nginx/html
# 2. Копируем твою папку js вручную (раз Vite её игнорирует)
# Мы берем её из /app/js (где она оказалась после COPY . .)
# и кладем в /usr/share/nginx/html/js
COPY --from=build /app/js /usr/share/nginx/html/js
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

14
README.md Executable file
View File

@@ -0,0 +1,14 @@
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

171
components/AboutUsSection.tsx Executable file
View File

@@ -0,0 +1,171 @@
import React, { useEffect } from 'react';
import { SECTION_IDS } from '../constants';
import { SparklesIcon } from './icons'; // Using an icon for principles
const principlesData = [
{
title: "Технологическое Лидерство",
content: "Мы не следуем трендам — мы стремимся их создавать, применяя самые передовые и эффективные подходы для решения задач."
},
{
title: "Ответственность и Этика",
content: "Мы осознаем влияние ИИ и разрабатываем безопасные, прозрачные и надежные системы, которые приносят пользу обществу."
},
{
title: "Фокус на Результате",
content: "Каждое наше решение, будь то сайт, приложение или сложная ИИ-модель, должно приносить измеримую пользу нашему партнеру. Мы ориентированы на ROI и практическую ценность."
},
{
title: "Открытое Партнерство",
content: "Великие проекты создаются в синергии. Мы открыты к сотрудничеству с государственными структурами, корпорациями, университетами и технологическими стартапами."
},
{
title: "Развитие Экосистемы",
content: "Наш вклад — это не только наши проекты, но и развитие ИИ-сообщества в Уфе и Башкортостане. Мы делимся знаниями, чтобы растить рынок вместе."
},
];
const approachData = [
{
title: "1. Прикладные ИИ-Решения",
content: "Мы разрабатываем и внедряем кастомные ИИ-решения для бизнеса и государства. <strong>Для государства:</strong> улучшение обработки обращений граждан, предиктивная аналитика. <strong>Для бизнеса:</strong> оптимизация логистики, персонализация продаж, автоматизация документооборота."
},
{
title: "2. Исследования и Разработки (R&D)",
content: "Адаптируем и дообучаем передовые модели для локальных задач. Особый фокус — развитие языковых моделей для русского и башкирского языков, создание датасетов и сохранение культурного наследия."
},
{
title: "3. Развитие Сообщества и Образование",
content: "Мы стремимся стать центром притяжения для талантов в Уфе. Организуем митапы, воркшопы и образовательные программы, формируя кадровый резерв для ИИ-индустрии региона."
}
];
const PrincipleItem: React.FC<{ title: string; children: React.ReactNode; index: number }> = ({ title, children, index }) => (
<li
className="flex items-start scroll-animate"
style={{ transitionDelay: `${index * 100}ms` }}
>
<div className="flex-shrink-0">
<SparklesIcon className="w-6 h-6 text-slate-500 mt-1" />
</div>
<div className="ml-4">
<h4 className="text-xl font-semibold font-quicksand text-slate-800">{title}</h4>
<p className="mt-1 text-slate-600">{children}</p>
</div>
</li>
);
const AboutUsSection: React.FC = () => {
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('scroll-animate-visible');
observer.unobserve(entry.target);
}
});
},
{
threshold: 0.1,
}
);
const elements = document.querySelectorAll('.scroll-animate');
elements.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
return (
<section
id={SECTION_IDS.aboutUsPage}
className="py-16 md:py-24 bg-white min-h-screen overflow-x-hidden"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-5xl">
{/* Header */}
<header className="mb-12 md:mb-16 text-center scroll-animate">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900 leading-tight">
iiEasy: Создаем Будущее с Помощью Искусственного Интеллекта.
</h1>
<p className="mt-4 text-2xl sm:text-3xl font-quicksand text-slate-600">Сделано в Уфе.</p>
</header>
{/* Intro */}
<div className="prose prose-lg max-w-none font-inter text-slate-700 mb-12 md:mb-16 scroll-animate" style={{ transitionDelay: '100ms' }}>
<p>Добро пожаловать в iiEasy центр передовых разработок и исследований в области искусственного интеллекта. Мы команда инженеров, исследователей и стратегов из Уфы, объединенных миссией по решению сложных задач для бизнеса и государства с помощью технологий. Мы не просто создаем сайты и приложения; мы проектируем интеллектуальные системы, которые оптимизируют процессы, улучшают принятие решений и открывают новые возможности для роста.</p>
<p>Наша работа основана на синтезе фундаментальной экспертизы в программной инженерии и глубокого понимания современных ИИ-архитектур, включая большие языковые модели (LLM), компьютерное зрение (CV) и предиктивную аналитику.</p>
</div>
<figure className="my-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
<img src="https://picsum.photos/seed/iieasy-team-work/1200/800" alt="Команда iiEasy за работой в современном офисе в Уфе, обсуждает проект на фоне доски с диаграммами и кодом. Атмосфера сфокусированной совместной работы." className="rounded-lg shadow-xl aspect-video object-cover"/>
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">Команда iiEasy в Уфе: от идеи к реализации.</figcaption>
</figure>
{/* Mission */}
<div className="my-16 md:my-24 text-center scroll-animate" style={{ transitionDelay: '100ms' }}>
<h2 className="font-quicksand text-3xl sm:text-4xl font-bold text-slate-800">Наша Миссия</h2>
<div className="prose prose-xl max-w-3xl mx-auto font-inter text-slate-700 mt-6">
<p className="font-semibold text-slate-800">Сделать Башкортостан лидирующим регионом в области искусственного интеллекта.</p>
<p>Мы стремимся к этой цели через решение практически значимых задач, создание продуктов мирового уровня и формирование сильного ИИ-сообщества. Мы верим, что технологии ИИ это ключ к повышению эффективности экономики, улучшению качества жизни граждан и созданию устойчивого будущего для нашего региона.</p>
</div>
</div>
{/* Approach */}
<div className="my-16 md:my-24 scroll-animate" style={{ transitionDelay: '200ms' }}>
<h2 className="font-quicksand text-3xl sm:text-4xl font-bold text-slate-800 mb-8 text-center">Наш Подход</h2>
<div className="prose prose-lg max-w-none font-inter text-slate-700 mb-6">
<p>Наша деятельность строится на трех ключевых направлениях:</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
{approachData.map((item, index) => (
<div key={item.title} className="bg-white p-6 rounded-lg scroll-animate" style={{ transitionDelay: `${index * 100}ms` }}>
<h3 className="font-quicksand text-xl font-semibold text-slate-800 mb-2">{item.title}</h3>
<p className="text-slate-600" dangerouslySetInnerHTML={{ __html: item.content }}></p>
</div>
))}
</div>
</div>
<figure className="my-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
<img src="https://picsum.photos/seed/iieasy-code/1200/800" alt="Крупный план экрана с кодом или дашбордом предиктивной аналитики. На заднем плане — сфокусированный взгляд разработчика, отражающийся в мониторе." className="rounded-lg shadow-xl aspect-video object-cover"/>
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">Глубокая техническая работа основа наших решений.</figcaption>
</figure>
{/* Team */}
<div className="my-16 md:my-24 scroll-animate" style={{ transitionDelay: '100ms' }}>
<h2 className="font-quicksand text-3xl sm:text-4xl font-bold text-slate-800 mb-6 text-center">Наша Команда</h2>
<div className="prose prose-lg max-w-none font-inter text-slate-700">
<p>Команда iiEasy это сплав системного мышления, инженерной прагматичности и стратегического видения. В наших рядах высококвалифицированные специалисты по машинному обучению, NLP, CV, анализу данных и разработке высоконагруженных систем. Нас объединяет культура постоянного самосовершенствования, нацеленность на результат и презрение к поверхностным решениям. Мы ценим скорость, смысл и ответственность за конечный продукт.</p>
</div>
</div>
<figure className="my-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
<img src="https://picsum.photos/seed/iieasy-team-group/1200/800" alt="Групповой снимок команды iiEasy. Разнообразные лица, уверенные и дружелюбные улыбки. Неформальная, но профессиональная обстановка." className="rounded-lg shadow-xl aspect-video object-cover"/>
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">Наша команда наш главный актив.</figcaption>
</figure>
{/* Principles */}
<div className="my-16 md:my-24 scroll-animate" style={{ transitionDelay: '100ms' }}>
<h2 className="font-quicksand text-3xl sm:text-4xl font-bold text-slate-800 mb-10 text-center">Наши Принципы</h2>
<ul className="space-y-8">
{principlesData.map((p, index) => (
<PrincipleItem key={p.title} title={p.title} index={index}>
{p.content}
</PrincipleItem>
))}
</ul>
</div>
<figure className="mt-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
<img src="https://picsum.photos/seed/iieasy-ufa-view/1200/800" alt="Панорамный вид на Уфу с акцентом на современные здания (например, Конгресс-холл Торатау), символизирующий связь компании с городом и ее нацеленность на будущее развитие региона." className="rounded-lg shadow-xl aspect-video object-cover"/>
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">Нацелены на будущее развитие нашего региона.</figcaption>
</figure>
</div>
</section>
);
};
export default AboutUsSection;

View File

@@ -0,0 +1,114 @@
import React from 'react';
import { SECTION_IDS, mockAcceleratorFeatures, ACCELERATOR_ABOUT_CONTENT } from '../constants'; // Import constants
import { AcceleratorFeature } from '../types'; // Import AcceleratorFeature type
import BoldIdeasCarousel from './BoldIdeasCarousel'; // Import the new component
// Icons for contact methods are now part of ACCELERATOR_ABOUT_CONTENT.contactMethods
interface FeatureCardProps {
icon: React.ElementType;
title: string;
description: string;
}
const FeatureCard: React.FC<FeatureCardProps> = ({ icon: Icon, title, description }) => (
<div className="bg-white p-6 rounded-xl">
<div className="flex items-center text-slate-700 mb-4">
<Icon className="w-8 h-8 mr-3" />
<h3 className="font-quicksand text-xl font-semibold text-slate-800">{title}</h3>
</div>
<p className="font-inter text-slate-600 leading-relaxed">
{description}
</p>
</div>
);
const BOLD_IDEAS_1 = [
"Создать ИИ-систему для мониторинга экологии Башкортостана",
"Разработать приложение для изучения башкирского языка с ИИ-помощником",
"Автоматизировать документооборот в государственных учреждениях",
"Платформа для предиктивной аналитики в сельском хозяйстве региона",
];
const BOLD_IDEAS_2 = [
"ИИ-ассистент для туристов, путешествующих по Уфе и окрестностям",
"Система компьютерного зрения для контроля качества на производстве",
"Адаптивная образовательная платформа для школьников",
"Разработать ИИ для оптимизации городской логистики в Уфе"
];
const AcceleratorAboutSection: React.FC = () => {
return (
<section
id={SECTION_IDS.acceleratorAboutPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-12 md:mb-16">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900">
{ACCELERATOR_ABOUT_CONTENT.title}
</h1>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-3xl">
{ACCELERATOR_ABOUT_CONTENT.subtitle}
</p>
</header>
<div className="prose prose-lg max-w-none font-inter text-slate-700 mb-12">
<h2 className="font-quicksand text-2xl sm:text-3xl font-semibold mt-0 mb-4 text-slate-800">{ACCELERATOR_ABOUT_CONTENT.whatWeOfferTitle}</h2>
<p>
{ACCELERATOR_ABOUT_CONTENT.whatWeOfferParagraph}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-16">
{mockAcceleratorFeatures.map((feature: AcceleratorFeature) => (
<FeatureCard
key={feature.title}
icon={feature.icon}
title={feature.title}
description={feature.description}
/>
))}
</div>
{/* New "Bold Ideas" Section */}
<div className="my-16 md:my-20">
<h2 className="text-center font-quicksand text-3xl sm:text-4xl font-semibold text-slate-700 mb-10">
У вас есть смелые идеи?
</h2>
<div className="space-y-3">
<BoldIdeasCarousel ideas={BOLD_IDEAS_1} direction="reverse" duration="80s" />
<BoldIdeasCarousel ideas={BOLD_IDEAS_2} direction="normal" duration="90s" />
</div>
</div>
<div className="text-center bg-white p-8 md:p-12 rounded-xl">
<h2 className="text-3xl font-quicksand font-semibold text-slate-800 mb-6">
{ACCELERATOR_ABOUT_CONTENT.ctaTitle}
</h2>
<p className="font-inter text-lg text-slate-600 mb-8 max-w-xl mx-auto">
{ACCELERATOR_ABOUT_CONTENT.ctaSubtitle}
</p>
<div className="flex flex-col sm:flex-row flex-wrap justify-center items-center gap-4 sm:gap-6">
{ACCELERATOR_ABOUT_CONTENT.contactMethods.map((method) => (
<a
key={method.id}
href={method.href}
target="_blank"
rel="noopener noreferrer"
className={`group inline-flex items-center justify-center px-6 py-3 text-base font-medium text-slate-800 ${method.bgColor} ${method.hoverBgColor} rounded-full focus:outline-none focus:ring-2 ${method.ringColor} focus:ring-offset-2 transition-all duration-150 ease-in-out font-quicksand w-full sm:w-auto sm:min-w-[180px]`}
aria-label={method.ariaLabel}
>
<method.icon className="w-5 h-5" />
<span className="ml-2.5">{method.name}</span>
</a>
))}
</div>
</div>
</div>
</section>
);
};
export default AcceleratorAboutSection;

View File

@@ -0,0 +1,130 @@
import React from 'react';
import { SECTION_IDS, ACCELERATOR_INVESTMENT_CONTENT } from '../constants';
import { InvestmentProject } from '../types';
import Button from './Button';
import { CurrencyDollarIcon, PresentationChartLineIcon, UsersIcon, LightBulbIcon } from '@heroicons/react/24/outline';
interface InvestmentCardProps {
project: InvestmentProject;
}
const InvestmentCard: React.FC<InvestmentCardProps> = ({ project }) => (
<div className="bg-white rounded-xl transition-all duration-300 ease-in-out flex flex-col overflow-hidden">
<div className="relative w-full aspect-[16/9] overflow-hidden">
<img src={project.imageUrl} alt={project.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" loading="lazy" />
</div>
<div className="p-5 flex flex-col flex-grow">
<h3 className="font-quicksand text-2xl font-semibold text-slate-800 mb-1">{project.name}</h3>
<p className="font-inter text-sm text-slate-600 mb-3">{project.industry}</p>
<div className="mb-3">
<h4 className="font-inter text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">{ACCELERATOR_INVESTMENT_CONTENT.problemLabel}</h4>
<div className="font-inter text-sm text-slate-600 line-clamp-2" dangerouslySetInnerHTML={{ __html: project.problem }}></div>
</div>
<div className="mb-4">
<h4 className="font-inter text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">{ACCELERATOR_INVESTMENT_CONTENT.solutionLabel}</h4>
<div className="font-inter text-sm text-slate-600 line-clamp-2" dangerouslySetInnerHTML={{ __html: project.solution }}></div>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm mb-4 font-inter text-slate-700">
<div className="flex items-center">
<UsersIcon className="w-4 h-4 mr-1.5 text-slate-500"/> {ACCELERATOR_INVESTMENT_CONTENT.teamLabel} {project.teamSize} чел.
</div>
<div className="flex items-center">
<CurrencyDollarIcon className="w-4 h-4 mr-1.5 text-slate-500"/> {ACCELERATOR_INVESTMENT_CONTENT.fundingLabel} {project.fundingSought}
</div>
</div>
{project.contactEmail ? (
<Button
variant="primary"
size="sm"
onClick={() => window.location.href = `mailto:${project.contactEmail}?subject=Инвестиции в ${project.name}`}
leftIcon={<PresentationChartLineIcon />}
className="mt-auto"
>
{ACCELERATOR_INVESTMENT_CONTENT.contactProjectButtonText}
</Button>
) : (
<Button
variant="secondary"
size="sm"
disabled
className="mt-auto"
>
{ACCELERATOR_INVESTMENT_CONTENT.requestContactButtonText}
</Button>
)}
</div>
</div>
);
interface AcceleratorInvestmentSectionProps {
investmentProjects: InvestmentProject[];
}
const AcceleratorInvestmentSection: React.FC<AcceleratorInvestmentSectionProps> = ({ investmentProjects }) => {
return (
<section
id={SECTION_IDS.acceleratorInvestmentPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-12 md:mb-16">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900">
{ACCELERATOR_INVESTMENT_CONTENT.title}
</h1>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-3xl">
{ACCELERATOR_INVESTMENT_CONTENT.subtitle}
</p>
</header>
{investmentProjects && investmentProjects.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{investmentProjects.map((project: InvestmentProject) => (
<InvestmentCard key={project.id} project={project} />
))}
</div>
) : (
<div className="text-center py-10 bg-white p-6 rounded-xl">
<CurrencyDollarIcon className="w-16 h-16 mx-auto text-slate-300 mb-4" />
<h2 className="text-2xl font-quicksand font-semibold text-slate-700 mb-3">{ACCELERATOR_INVESTMENT_CONTENT.emptyStateTitle}</h2>
<p className="text-slate-600 font-inter">
{ACCELERATOR_INVESTMENT_CONTENT.emptyStateMessage}
</p>
</div>
)}
<div className="mt-16 text-center bg-white p-8 md:p-12 rounded-xl">
<LightBulbIcon className="w-12 h-12 mx-auto text-slate-500 mb-4" />
<h2 className="text-3xl font-quicksand font-semibold text-slate-800 mb-6">
{ACCELERATOR_INVESTMENT_CONTENT.investorCtaTitle}
</h2>
<p className="font-inter text-lg text-slate-600 mb-8 max-w-xl mx-auto">
{ACCELERATOR_INVESTMENT_CONTENT.investorCtaSubtitle}
</p>
<div className="flex flex-col sm:flex-row flex-wrap justify-center items-center gap-4 sm:gap-6">
{ACCELERATOR_INVESTMENT_CONTENT.contactMethods.map((method) => (
<a
key={method.id}
href={method.href}
target="_blank"
rel="noopener noreferrer"
className={`group inline-flex items-center justify-center px-6 py-3 text-base font-medium text-slate-800 ${method.bgColor} ${method.hoverBgColor} rounded-full focus:outline-none focus:ring-2 ${method.ringColor} focus:ring-offset-2 transition-all duration-150 ease-in-out font-quicksand w-full sm:w-auto sm:min-w-[180px]`}
aria-label={method.ariaLabel}
>
<method.icon className="w-5 h-5" />
<span className="ml-2.5">{method.name}</span>
</a>
))}
</div>
</div>
</div>
</section>
);
};
export default AcceleratorInvestmentSection;

View File

@@ -0,0 +1,85 @@
import React from 'react';
import { SECTION_IDS, ACCELERATOR_PROJECTS_CONTENT } from '../constants';
import { AcceleratorProject } from '../types';
import Button from './Button';
import { CubeTransparentIcon, TagIcon, LinkIcon } from '@heroicons/react/24/outline';
interface ProjectCardProps {
project: AcceleratorProject;
}
const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => (
<div className="bg-white rounded-xl transition-all duration-300 ease-in-out flex flex-col overflow-hidden">
<div className="relative w-full aspect-[16/9] overflow-hidden">
<img src={project.imageUrl} alt={project.name} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" loading="lazy" />
</div>
<div className="p-5 flex flex-col flex-grow">
<h3 className="font-quicksand text-2xl font-semibold text-slate-800 mb-1">{project.name}</h3>
<p className="font-inter text-sm text-slate-600 mb-2">{project.tagline}</p>
<div className="flex items-center text-xs text-slate-500 mb-3">
<TagIcon className="w-4 h-4 mr-1 text-slate-500" />
<span>{project.category}</span>
<span className="mx-1.5"></span>
<span className="px-2 py-0.5 rounded-full text-xs font-semibold bg-slate-100 text-slate-700">
{project.statuspro}
</span>
</div>
<div className="font-inter text-slate-600 leading-relaxed line-clamp-3 mb-4 flex-grow" dangerouslySetInnerHTML={{ __html: project.description }}></div>
{project.websiteUrl && (
<Button
variant="outline"
size="sm"
onClick={() => window.open(project.websiteUrl, '_blank')}
leftIcon={<LinkIcon />}
className="mt-auto"
>
{ACCELERATOR_PROJECTS_CONTENT.projectWebsiteButtonText}
</Button>
)}
</div>
</div>
);
interface AcceleratorProjectsSectionProps {
acceleratorProjects: AcceleratorProject[];
}
const AcceleratorProjectsSection: React.FC<AcceleratorProjectsSectionProps> = ({ acceleratorProjects }) => {
return (
<section
id={SECTION_IDS.acceleratorProjectsPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-12 md:mb-16">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900">
{ACCELERATOR_PROJECTS_CONTENT.title}
</h1>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-3xl">
{ACCELERATOR_PROJECTS_CONTENT.subtitle}
</p>
</header>
{acceleratorProjects && acceleratorProjects.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{acceleratorProjects.map((project: AcceleratorProject) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
) : (
<div className="text-center py-10 bg-white p-6 rounded-xl">
<CubeTransparentIcon className="w-16 h-16 mx-auto text-slate-300 mb-4" />
<h2 className="text-2xl font-quicksand font-semibold text-slate-700 mb-3">{ACCELERATOR_PROJECTS_CONTENT.emptyStateTitle}</h2>
<p className="text-slate-600 font-inter">
{ACCELERATOR_PROJECTS_CONTENT.emptyStateMessage}
</p>
</div>
)}
</div>
</section>
);
};
export default AcceleratorProjectsSection;

86
components/AdminLoginPage.tsx Executable file
View File

@@ -0,0 +1,86 @@
import React, { useState } from 'react';
import Button from './Button';
import Logo from './Logo';
import { APP_NAME } from '../constants';
interface AdminLoginPageProps {
onLoginSuccess: () => void;
}
// SHA-256 hash of "IZI348ax@" is 8b1f38e07289947545ea88540871147573199ce071804100412e0b5f590d7c71
const STORED_HASH = '8b1f38e07289947545ea88540871147573199ce071804100412e0b5f590d7c71';
async function sha256(message: string): Promise<string> {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
const AdminLoginPage: React.FC<AdminLoginPageProps> = ({ onLoginSuccess }) => {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
const inputHash = await sha256(password);
if (inputHash === STORED_HASH) {
onLoginSuccess();
} else {
setError('Неверный пароль. Пожалуйста, попробуйте снова.');
}
setIsLoading(false);
setPassword('');
};
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-white p-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<Logo width={120} height={90} className="mx-auto" />
<h1 className="mt-6 text-3xl font-quicksand font-bold text-slate-800">
Админ-панель {APP_NAME}
</h1>
<p className="mt-2 text-sm text-slate-600">
Редактирование констант приложения
</p>
</div>
<form onSubmit={handleSubmit} className="bg-white rounded-xl p-8 space-y-6">
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-700 font-quicksand">
Пароль
</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full px-3 py-2 bg-white border border-slate-300 rounded-md placeholder-slate-400 focus:outline-none focus:ring-slate-500 focus:border-slate-500 sm:text-sm text-slate-900"
placeholder="Введите ваш пароль"
/>
</div>
{error && <p className="text-xs text-red-600">{error}</p>}
<Button
type="submit"
variant="primary"
size="lg"
className="w-full"
disabled={isLoading}
>
{isLoading ? 'Вход...' : 'Войти'}
</Button>
</form>
<p className="mt-8 text-center text-xs text-slate-500">
Только для авторизованного персонала. Все действия могут отслеживаться.
</p>
</div>
</div>
);
};
export default AdminLoginPage;

31
components/ApproachCard.tsx Executable file
View File

@@ -0,0 +1,31 @@
import React from 'react';
import { PlusIcon } from './icons';
interface ApproachCardProps {
title: string;
description: string;
visual: React.ReactNode;
}
const ApproachCard: React.FC<ApproachCardProps> = ({ title, description, visual }) => {
return (
<div className="border border-slate-200 p-6 rounded-lg group flex flex-col h-full bg-white">
<h3 className="text-xl font-semibold font-quicksand text-slate-800">{title}</h3>
<div className="mt-4 flex-grow flex items-center justify-center min-h-[12rem]">
{visual}
</div>
<div className="mt-4 flex-grow flex flex-col">
<p className="text-slate-600 font-inter text-sm flex-grow">{description}</p>
<button
type="button"
className="mt-4 flex items-center justify-center w-8 h-8 rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700 transition-colors"
aria-label={`Learn more about ${title}`}
>
<PlusIcon className="w-4 h-4" />
</button>
</div>
</div>
);
};
export default ApproachCard;

178
components/BlogPostDetail.tsx Executable file
View File

@@ -0,0 +1,178 @@
import React, { useRef, useState } from 'react';
import { CurrentView, Post } from '../types';
import { SECTION_IDS, BLOG_POST_DETAIL_CONTENT, UI_TEXTS } from '../constants'; // Removed mockPosts
import Button from './Button';
import { ArrowUturnLeftIcon, PlayIcon, PauseIcon, ChevronRightIcon } from './icons';
import ItemDetailNavigation from './ItemDetailNavigation';
import PostCard from './PostCard';
import GalleryComponent from './GalleryComponent'; // Import GalleryComponent
interface BlogPostDetailProps {
itemId: string;
allPosts: Post[];
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const BlogPostDetail: React.FC<BlogPostDetailProps> = ({ itemId, allPosts, setCurrentView, setSelectedItemId }) => {
const post = allPosts.find(p => p.id === itemId);
const videoRef = useRef<HTMLVideoElement>(null);
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
const handleBackClick = () => {
setCurrentView('storiesAll');
setSelectedItemId(null);
};
const handleNavigateItem = (newItemId: string) => {
setSelectedItemId(newItemId);
};
const handleShowAllKeepReading = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
setCurrentView('storiesAll');
setSelectedItemId(null);
};
if (!post) {
return (
<section id={SECTION_IDS.blogPostDetailPage} className="py-16 md:py-24 min-h-screen flex items-center justify-center">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="text-2xl font-semibold text-slate-700">{BLOG_POST_DETAIL_CONTENT.notFoundTitle}</h1>
<Button onClick={handleBackClick} leftIcon={<ArrowUturnLeftIcon />} className="mt-8">
{BLOG_POST_DETAIL_CONTENT.backButtonText}
</Button>
</div>
</section>
);
}
const currentIndex = allPosts.findIndex(p => p.id === itemId);
const previousPost = currentIndex > 0 ? allPosts[currentIndex - 1] : undefined;
const nextPost = currentIndex < allPosts.length - 1 ? allPosts[currentIndex + 1] : undefined;
const relatedPosts = allPosts
.filter(p => p.id !== itemId)
.slice(0, 3);
const toggleVideoPlay = (e: React.MouseEvent) => {
e.stopPropagation();
if (videoRef.current) {
if (videoRef.current.paused) {
videoRef.current.play();
setIsVideoPlaying(true);
} else {
videoRef.current.pause();
setIsVideoPlaying(false);
}
}
};
return (
<section
id={SECTION_IDS.blogPostDetailPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<Button
variant="outline"
size="sm"
onClick={handleBackClick}
leftIcon={<ArrowUturnLeftIcon />}
className="mb-6"
>
{BLOG_POST_DETAIL_CONTENT.backButtonText}
</Button>
{post.videoUrl ? (
<div className="mb-8 rounded-lg overflow-hidden relative aspect-video">
<video
ref={videoRef}
controls
className="w-full h-full object-cover"
poster={post.imageUrl}
onPlay={() => setIsVideoPlaying(true)}
onPause={() => setIsVideoPlaying(false)}
>
<source src={post.videoUrl} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
) : post.imageUrl && (
<div className="mb-8 rounded-lg overflow-hidden">
<img src={post.imageUrl} alt={post.title} className="w-full h-auto object-cover max-h-[60vh]" />
</div>
)}
<div className="max-w-3xl mx-auto">
<header className="mb-8 md:mb-12">
<h1 className="font-quicksand text-3xl sm:text-4xl md:text-5xl font-bold text-slate-800 mb-3">
{post.title}
</h1>
<div className="font-inter text-sm text-slate-500">
<span className="font-semibold text-slate-600">{post.category}</span>
{(post.date || post.readTime) && <span className="mx-1.5"></span>}
{post.date && <span>{post.date}</span>}
{post.date && post.readTime && <span className="mx-1.5"></span>}
{post.readTime && <span>{post.readTime}</span>}
</div>
</header>
<article className="prose prose-lg max-w-none font-inter text-slate-700">
{typeof post.fullContent === 'string' ? (
<div dangerouslySetInnerHTML={{ __html: post.fullContent }} />
) : (
post.fullContent
)}
</article>
{post.gallery && post.gallery.length > 0 && (
<GalleryComponent items={post.gallery} title={BLOG_POST_DETAIL_CONTENT.galleryTitle} />
)}
<ItemDetailNavigation
previousItem={previousPost ? { id: previousPost.id, title: previousPost.title } : undefined}
nextItem={nextPost ? { id: nextPost.id, title: nextPost.title } : undefined}
onNavigate={handleNavigateItem}
previousItemButtonLabel={BLOG_POST_DETAIL_CONTENT.previousItemButtonLabel || `Предыдущий ${BLOG_POST_DETAIL_CONTENT.itemTypeSingular}`}
nextItemButtonLabel={BLOG_POST_DETAIL_CONTENT.nextItemButtonLabel || `Следующий ${BLOG_POST_DETAIL_CONTENT.itemTypeSingular}`}
noPreviousItemText={BLOG_POST_DETAIL_CONTENT.noPreviousItemText || `Это первая история`}
noNextItemText={BLOG_POST_DETAIL_CONTENT.noNextItemText || `Это последняя история`}
themeColorClass="slate"
/>
</div>
{relatedPosts.length > 0 && (
<div className="mt-12 md:mt-16">
<div className="flex justify-between items-baseline mb-8">
<h2 className="font-quicksand text-3xl font-bold text-slate-800">
{BLOG_POST_DETAIL_CONTENT.keepReadingTitle || "Читайте также"}
</h2>
<a
href={`#${SECTION_IDS.storiesAllPage}`}
onClick={handleShowAllKeepReading}
className="group inline-flex items-center text-base font-medium text-slate-700 hover:text-black transition-colors whitespace-nowrap"
aria-label={`Посмотреть все ${BLOG_POST_DETAIL_CONTENT.itemTypePlural}`}
>
{BLOG_POST_DETAIL_CONTENT.viewAllButtonText || `Все ${BLOG_POST_DETAIL_CONTENT.itemTypePlural}`}
<ChevronRightIcon className="ml-1 w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" />
</a>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{relatedPosts.map(relatedPost => (
<PostCard
key={relatedPost.id}
post={relatedPost}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
/>
))}
</div>
</div>
)}
</div>
</section>
);
};
export default BlogPostDetail;

98
components/BlogSection.tsx Executable file
View File

@@ -0,0 +1,98 @@
import React from 'react';
import { CurrentView, Post } from '../types'; // Added Post type
import { SECTION_IDS, BLOG_SECTION_CONTENT } from '../constants';
import { ChevronRightIcon } from './icons';
import PostCard from './PostCard';
interface BlogSectionProps {
posts: Post[]; // Changed from mockPosts to accept fetched data
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const BlogSection: React.FC<BlogSectionProps> = ({ posts, setCurrentView, setSelectedItemId }) => {
const featuredPost = posts.find(p => p.isFeatured);
const otherPosts = posts.filter(p => !p.isFeatured);
const handleShowAllClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
setCurrentView('storiesAll');
setSelectedItemId(null);
};
// App.tsx handles the main loading and error states.
// This component now assumes that if `posts` is empty, it's an empty data state, not a loading state.
if (posts.length === 0) {
return (
<section id={SECTION_IDS.story} className="py-16 md:py-24 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-baseline mb-12 md:mb-16">
<h2 className="font-quicksand text-4xl sm:text-5xl font-bold text-slate-900">
{BLOG_SECTION_CONTENT.title}
</h2>
<a
href={`#${SECTION_IDS.storiesAllPage}`}
onClick={handleShowAllClick}
className="group inline-flex items-center text-base font-medium text-slate-700 hover:text-black transition-colors whitespace-nowrap"
aria-label={BLOG_SECTION_CONTENT.showAllAriaLabel}
>
{BLOG_SECTION_CONTENT.showAllText}
<ChevronRightIcon className="ml-1 w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" />
</a>
</div>
<div className="text-center py-10">
<p className="text-slate-600 font-inter text-lg">Проектов пока нет.</p>
<p className="text-slate-500 font-inter mt-2">Возможно, они еще не опубликованы. Пожалуйста, проверьте позже.</p>
</div>
</div>
</section>
);
}
return (
<section id={SECTION_IDS.story} className="py-16 md:py-24 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-baseline mb-12 md:mb-16">
<h2 className="font-quicksand text-4xl sm:text-5xl font-bold text-slate-900">
{BLOG_SECTION_CONTENT.title}
</h2>
<a
href={`#${SECTION_IDS.storiesAllPage}`}
onClick={handleShowAllClick}
className="group inline-flex items-center text-base font-medium text-slate-700 hover:text-black transition-colors whitespace-nowrap"
aria-label={BLOG_SECTION_CONTENT.showAllAriaLabel}
>
{BLOG_SECTION_CONTENT.showAllText}
<ChevronRightIcon className="ml-1 w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" />
</a>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8 xl:gap-12">
{featuredPost && (
<div className="lg:col-span-3 lg:sticky lg:top-24 self-start">
<PostCard
post={featuredPost}
isFeatured
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
/>
</div>
)}
<div className={`lg:col-span-1 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-1 gap-6 lg:gap-8 ${featuredPost ? '' : 'lg:col-span-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'}`}>
{otherPosts.slice(featuredPost ? -3 : -4).reverse().map(post => (
<PostCard
key={post.id}
post={post}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
/>
))}
</div>
</div>
</div>
</section>
);
};
export default BlogSection;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { ArrowTopRightIcon } from './icons';
interface BoldIdeasCarouselProps {
ideas: string[];
direction?: 'normal' | 'reverse';
duration?: string;
}
const BoldIdeasCarousel: React.FC<BoldIdeasCarouselProps> = ({ ideas, direction = 'normal', duration = '72s' }) => {
return (
<div className="w-full overflow-hidden">
<div
className="flex w-fit"
style={{ animation: `auto-scroll ${duration} linear infinite`, animationDirection: direction }}
>
{/* Render the list twice for seamless loop */}
{[...ideas, ...ideas].map((idea, index) => (
<div key={`${direction}-idea-${index}`} className="group relative mx-1 flex w-72 items-center justify-center self-stretch rounded-md bg-slate-50 p-4 transition-all hover:bg-slate-100">
<a href="#" onClick={(e) => e.preventDefault()} className="text-sm text-center text-slate-700 font-medium cursor-default">
{idea}
<ArrowTopRightIcon className="ml-1.5 inline-block h-3 w-3 -translate-y-px transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-1" />
</a>
</div>
))}
</div>
</div>
);
};
export default BoldIdeasCarousel;

85
components/BusinessCard.tsx Executable file
View File

@@ -0,0 +1,85 @@
import React from 'react';
import { BusinessStory, CurrentView } from '../types';
import { ViewMode } from './FilterSortBar';
interface BusinessCardProps {
story: BusinessStory;
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
className?: string;
viewMode?: ViewMode;
}
const BusinessCard: React.FC<BusinessCardProps> = ({ story, setCurrentView, setSelectedItemId, className = "", viewMode = 'grid' }) => {
const handleCardClick = () => {
setCurrentView('businessStoryDetail');
setSelectedItemId(story.id);
};
if (viewMode === 'list') {
return (
<div
onClick={handleCardClick}
className={`group block p-4 rounded-lg bg-white transition-all duration-300 ease-in-out hover:bg-slate-50 cursor-pointer w-full ${className}`}
role="article"
aria-labelledby={`business-title-${story.id}`}
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0 w-24 h-24 rounded-md overflow-hidden bg-slate-200">
<img
src={story.imageUrl}
alt={`Изображение для: ${story.title}`}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 ease-in-out"
loading="lazy"
/>
</div>
<div className="flex-1">
<h3 id={`business-title-${story.id}`} className="text-base md:text-lg font-quicksand font-semibold text-slate-800 group-hover:text-black mb-1 transition-colors duration-200 leading-tight">
{story.title}
</h3>
<p className="text-xs text-slate-500 mb-2">
<span className="font-semibold text-slate-600">{story.category}</span>
</p>
{story.description && (
<p className="text-sm text-slate-600 line-clamp-2 font-inter">{story.description}</p>
)}
</div>
</div>
</div>
);
}
// Grid view
return (
<div
onClick={handleCardClick}
className={`group relative bg-white rounded-xl overflow-hidden transition-all duration-300 ease-in-out transform hover:-translate-y-1 cursor-pointer shadow-sm hover:shadow-md ${className}`}
role="article"
aria-labelledby={`business-title-${story.id}`}
>
<div className="relative w-full overflow-hidden aspect-[4/3]">
<img
src={story.imageUrl}
alt={`Изображение для: ${story.title}`}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 ease-in-out"
loading="lazy"
/>
</div>
<div className="p-4 md:p-5">
<p className="text-xs text-slate-600 font-semibold mb-1">
{story.category}
</p>
<h3 id={`business-title-${story.id}`} className="font-quicksand font-semibold text-md md:text-lg text-slate-800 group-hover:text-black mb-1 transition-colors duration-200 leading-tight">
{story.title}
</h3>
{story.description && (
<p className="text-sm text-slate-600 mt-2 line-clamp-3 font-inter">{story.description}</p>
)}
</div>
</div>
);
};
export default BusinessCard;

View File

@@ -0,0 +1,118 @@
import React, { useState, useMemo } from 'react';
import { CurrentView, BusinessStory } from '../types';
import { SECTION_IDS, BUSINESS_LANDING_CONTENT } from '../constants';
import BusinessCard from './BusinessCard';
import FilterSortBar, { ViewMode, SortOptionKey, SortOption } from './FilterSortBar';
interface BusinessLandingSectionProps {
businessStories: BusinessStory[];
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const sortOptions: SortOption[] = [
{ key: 'alphabetical', label: 'По алфавиту (А-Я)' },
];
const BusinessLandingSection: React.FC<BusinessLandingSectionProps> = ({ businessStories, setCurrentView, setSelectedItemId }) => {
const [viewMode, setViewMode] = useState<ViewMode>('grid');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedSort, setSelectedSort] = useState<SortOptionKey>('alphabetical');
const availableCategories = useMemo(() => {
if (!businessStories) return [];
return Array.from(new Set(businessStories.map(p => p.category))).sort();
}, [businessStories]);
const handleCategoryChange = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const filteredAndSortedStories = useMemo(() => {
if (!businessStories) return [];
const filtered = selectedCategories.length > 0
? businessStories.filter(p => selectedCategories.includes(p.category))
: businessStories;
return [...filtered].sort((a, b) => {
if (selectedSort === 'alphabetical') {
return a.title.localeCompare(b.title);
}
return 0; // Only alphabetical sort is supported
});
}, [businessStories, selectedCategories, selectedSort]);
if (!businessStories) {
return (
<section
id={SECTION_IDS.businessLandingPage}
className="py-16 md:py-24 bg-white min-h-screen flex justify-center items-center"
>
<p className="text-slate-600 font-inter">Загрузка кейсов...</p>
</section>
);
}
const layoutClasses = viewMode === 'grid'
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
: "flex flex-col gap-4";
return (
<section
id={SECTION_IDS.businessLandingPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-12 md:mb-16">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900">
{BUSINESS_LANDING_CONTENT.title}
</h1>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-3xl">
{BUSINESS_LANDING_CONTENT.subtitle}
</p>
</header>
<FilterSortBar
viewMode={viewMode}
onViewChange={setViewMode}
availableCategories={availableCategories}
selectedCategories={selectedCategories}
onCategoryChange={handleCategoryChange}
onClearCategories={() => setSelectedCategories([])}
sortOptions={sortOptions}
selectedSort={selectedSort}
onSortChange={setSelectedSort}
/>
{filteredAndSortedStories.length > 0? (
<div className={layoutClasses}>
{filteredAndSortedStories.map(story => (
<BusinessCard
key={story.id}
story={story}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
viewMode={viewMode}
/>
))}
</div>
) : (
<div className="text-center py-10 bg-white p-6 rounded-xl">
<h2 className="text-2xl font-quicksand font-semibold text-slate-700 mb-3">{BUSINESS_LANDING_CONTENT.emptyStateTitle}</h2>
<p className="text-slate-600 font-inter">
{BUSINESS_LANDING_CONTENT.emptyStateMessage}
</p>
</div>
)}
</div>
</section>
);
};
export default BusinessLandingSection;

67
components/BusinessSection.tsx Executable file
View File

@@ -0,0 +1,67 @@
import React from 'react';
import { CurrentView, BusinessStory } from '../types';
import { SECTION_IDS, BUSINESS_SECTION_CONTENT } from '../constants';
import { ChevronRightIcon } from './icons';
import BusinessCard from './BusinessCard';
interface BusinessSectionProps {
businessStories: BusinessStory[]; // Added to accept fetched data
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const BusinessSection: React.FC<BusinessSectionProps> = ({ businessStories, setCurrentView, setSelectedItemId }) => {
const handleShowAllClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
setCurrentView('businessLanding');
setSelectedItemId(null);
};
if (!businessStories || businessStories.length === 0) {
return (
<section id={SECTION_IDS.businessShowcase} className="py-16 md:py-24 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
{/* Optionally, you can add a loading or empty state specific to this section */}
</div>
</section>
);
}
return (
<section id={SECTION_IDS.businessShowcase} className="py-16 md:py-24 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-baseline mb-10 md:mb-12">
<h2 className="font-quicksand text-4xl sm:text-5xl font-bold text-slate-900">
{BUSINESS_SECTION_CONTENT.title}
</h2>
<a
href={`#${SECTION_IDS.businessLandingPage}`}
onClick={handleShowAllClick}
className="group inline-flex items-center text-base font-medium text-slate-700 hover:text-black transition-colors whitespace-nowrap"
aria-label={BUSINESS_SECTION_CONTENT.showAllAriaLabel}
>
{BUSINESS_SECTION_CONTENT.showAllText}
<ChevronRightIcon className="ml-1 w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" />
</a>
</div>
</div>
<div className="w-full overflow-x-auto overflow-y-hidden no-scrollbar snap-x snap-mandatory">
<div className="flex flex-nowrap gap-4 sm:gap-6 px-4 sm:px-6 lg:px-8 min-w-max pb-2 -mb-2">
{businessStories.slice(-6).reverse().map(story => ( // Show up to 6 stories in the scroller
<BusinessCard
key={story.id}
story={story}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
className="flex-shrink-0 w-72 sm:w-80 md:w-[22rem] snap-start"
/>
))}
</div>
</div>
</section>
);
};
export default BusinessSection;

View File

@@ -0,0 +1,153 @@
import React from 'react';
import { SECTION_IDS, BUSINESS_SERVICES_CONTENT } from '../constants';
import { ServiceItemData, ClientLogo } from '../types';
import {
ComputerDesktopIcon, CodeBracketIcon, CpuChipIcon, MagnifyingGlassIcon,
PaintBrushIcon, ShieldCheckIcon, UsersIcon, LightBulbIcon, ChartBarIcon, AcademicCapIcon,
PuzzlePieceIcon, CommandLineIcon, BriefcaseIcon, BuildingOffice2Icon, BuildingLibraryIcon,
CircleStackIcon, CurrencyDollarIcon, HomeIcon, InformationCircleIcon, NewspaperIcon, RocketLaunchIcon,
SparklesIcon as SoraIcon, WrenchScrewdriverIcon
} from '../components/icons'; // Ensure all potential icons are imported
// Define an icon map
const iconMap: Record<string, React.ElementType> = {
ComputerDesktopIcon,
CodeBracketIcon,
CpuChipIcon,
MagnifyingGlassIcon,
PaintBrushIcon,
ShieldCheckIcon,
UsersIcon,
LightBulbIcon,
ChartBarIcon,
AcademicCapIcon,
PuzzlePieceIcon,
CommandLineIcon,
BriefcaseIcon,
BuildingOffice2Icon,
BuildingLibraryIcon,
CircleStackIcon,
CurrencyDollarIcon,
HomeIcon,
InformationCircleIcon,
NewspaperIcon,
RocketLaunchIcon,
SoraIcon,
WrenchScrewdriverIcon,
// Add other icons as needed, ensure keys match Strapi values
};
interface ServiceItemProps {
iconName: string; // Changed from icon: React.ElementType to iconName: string
title: string;
description: string;
}
const ServiceItem: React.FC<ServiceItemProps> = ({ iconName, title, description }) => {
const IconComponent = iconMap[iconName] || CpuChipIcon; // Fallback to a default icon
return (
<div className="bg-white p-6 rounded-xl">
<div className="flex items-start">
<div className="flex-shrink-0">
<IconComponent className="w-8 h-8 text-slate-600" aria-hidden="true" />
</div>
<div className="ml-4">
<h3 className="font-quicksand text-xl font-semibold text-slate-800 mb-2">{title}</h3>
<p className="font-inter text-slate-600 leading-relaxed">
{description}
</p>
</div>
</div>
</div>
);
};
interface BusinessServicesSectionProps {
serviceItems: ServiceItemData[];
clientLogos: ClientLogo[];
}
const BusinessServicesSection: React.FC<BusinessServicesSectionProps> = ({ serviceItems, clientLogos }) => {
return (
<section
id={SECTION_IDS.businessServicesPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-12 md:mb-16">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900">
{BUSINESS_SERVICES_CONTENT.title}
</h1>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-3xl">
{BUSINESS_SERVICES_CONTENT.subtitle}
</p>
</header>
{serviceItems && serviceItems.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{serviceItems.map((service) => (
<ServiceItem
key={service.title}
iconName={service.icon} // Pass iconName string
title={service.title}
description={service.description}
/>
))}
</div>
) : (
<div className="text-center py-10">
<WrenchScrewdriverIcon className="w-12 h-12 mx-auto text-slate-300 mb-4" />
<p className="text-slate-600 font-inter">Услуги скоро будут добавлены.</p>
</div>
)}
{clientLogos && clientLogos.length > 0 && (
<div className="mt-16 md:mt-24">
<h2 className="text-center font-quicksand text-3xl sm:text-4xl font-semibold text-slate-700 mb-10">
{BUSINESS_SERVICES_CONTENT.trustedByTitle}
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-x-6 gap-y-8 items-center">
{clientLogos.map((logo) => (
<div key={logo.id} className="flex justify-center items-center group">
<img
src={logo.imageUrl}
alt={logo.name}
className="h-12 md:h-16 object-contain filter grayscale group-hover:grayscale-0 transition-all duration-300 ease-in-out"
loading="lazy"
/>
</div>
))}
</div>
</div>
)}
<div className="mt-16 md:mt-24 text-center bg-white p-8 md:p-12 rounded-xl">
<h2 className="text-3xl font-quicksand font-semibold text-slate-800 mb-6">
{BUSINESS_SERVICES_CONTENT.ctaTitle}
</h2>
<p className="font-inter text-lg text-slate-600 mb-8 max-w-xl mx-auto">
{BUSINESS_SERVICES_CONTENT.ctaSubtitle}
</p>
<div className="flex flex-col sm:flex-row flex-wrap justify-center items-center gap-4 sm:gap-6">
{BUSINESS_SERVICES_CONTENT.contactMethods.map((method) => (
<a
key={method.id}
href={method.href}
target="_blank"
rel="noopener noreferrer"
className={`group inline-flex items-center justify-center px-6 py-3 text-base font-medium text-slate-800 ${method.bgColor} ${method.hoverBgColor} rounded-full focus:outline-none focus:ring-2 ${method.ringColor} focus:ring-offset-2 transition-all duration-150 ease-in-out font-quicksand w-full sm:w-auto sm:min-w-[180px]`}
aria-label={method.ariaLabel}
>
<method.icon className="w-5 h-5" />
<span className="ml-2.5">{method.name}</span>
</a>
))}
</div>
</div>
</div>
</section>
);
};
export default BusinessServicesSection;

View File

@@ -0,0 +1,151 @@
import React from 'react';
import { CurrentView, BusinessStory } from '../types';
import { SECTION_IDS, BUSINESS_STORY_DETAIL_CONTENT } from '../constants';
import Button from './Button';
import { ArrowUturnLeftIcon, ChevronRightIcon } from './icons';
import ItemDetailNavigation from './ItemDetailNavigation';
import BusinessCard from './BusinessCard';
import GalleryComponent from './GalleryComponent'; // Import GalleryComponent
interface BusinessStoryDetailProps {
itemId: string;
allBusinessStories: BusinessStory[];
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const BusinessStoryDetail: React.FC<BusinessStoryDetailProps> = ({ itemId, allBusinessStories, setCurrentView, setSelectedItemId }) => {
const story = allBusinessStories.find(p => p.id === itemId);
const handleBackClick = () => {
setCurrentView('businessLanding');
setSelectedItemId(null);
};
const handleNavigateItem = (newItemId: string) => {
setSelectedItemId(newItemId);
};
const handleShowAllKeepReading = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
setCurrentView('businessLanding');
setSelectedItemId(null);
};
if (!story) {
return (
<section id={SECTION_IDS.businessStoryDetail} className="py-16 md:py-24 min-h-screen flex items-center justify-center">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="text-2xl font-semibold text-slate-700">{BUSINESS_STORY_DETAIL_CONTENT.notFoundTitle}</h1>
<Button onClick={handleBackClick} leftIcon={<ArrowUturnLeftIcon />} className="mt-8">
{BUSINESS_STORY_DETAIL_CONTENT.backButtonText}
</Button>
</div>
</section>
);
}
const currentIndex = allBusinessStories.findIndex(s => s.id === itemId);
const previousStory = currentIndex > 0 ? allBusinessStories[currentIndex - 1] : undefined;
const nextStory = currentIndex < allBusinessStories.length - 1 ? allBusinessStories[currentIndex + 1] : undefined;
const relatedStories = allBusinessStories
.filter(s => s.id !== itemId)
.slice(0, 3);
return (
<section
id={SECTION_IDS.businessStoryDetail}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<Button
variant="outline"
size="sm"
onClick={handleBackClick}
leftIcon={<ArrowUturnLeftIcon />}
className="mb-6"
>
{BUSINESS_STORY_DETAIL_CONTENT.backButtonText}
</Button>
{story.imageUrl && (
<div className="mb-8 rounded-lg overflow-hidden">
<img src={story.imageUrl} alt={story.title} className="w-full h-auto object-cover max-h-[50vh]" />
</div>
)}
<div className="max-w-3xl mx-auto">
<header className="mb-8 md:mb-12">
<h1 className="font-quicksand text-3xl sm:text-4xl md:text-5xl font-bold text-slate-800 mb-3">
{story.title}
</h1>
<div className="font-inter text-sm text-slate-500">
<span className="font-semibold text-slate-600">{story.category}</span>
</div>
</header>
<article className="prose prose-lg max-w-none font-inter text-slate-700">
{story.description && <p className="lead text-xl">{story.description}</p>}
{typeof story.fullContent === 'string' ? (
<div dangerouslySetInnerHTML={{ __html: story.fullContent }} />
) : (
story.fullContent
)}
</article>
{story.gallery && story.gallery.length > 0 && (
<GalleryComponent items={story.gallery} title={BUSINESS_STORY_DETAIL_CONTENT.galleryTitle} />
)}
<ItemDetailNavigation
previousItem={previousStory ? { id: previousStory.id, title: previousStory.title } : undefined}
nextItem={nextStory ? { id: nextStory.id, title: nextStory.title } : undefined}
onNavigate={handleNavigateItem}
previousItemButtonLabel={BUSINESS_STORY_DETAIL_CONTENT.previousItemButtonLabel || `Предыдущий ${BUSINESS_STORY_DETAIL_CONTENT.itemTypeSingular}`}
nextItemButtonLabel={BUSINESS_STORY_DETAIL_CONTENT.nextItemButtonLabel || `Следующий ${BUSINESS_STORY_DETAIL_CONTENT.itemTypeSingular}`}
noPreviousItemText={BUSINESS_STORY_DETAIL_CONTENT.noPreviousItemText || `Это первый ${BUSINESS_STORY_DETAIL_CONTENT.itemTypeSingular}`}
noNextItemText={BUSINESS_STORY_DETAIL_CONTENT.noNextItemText || `Это последний ${BUSINESS_STORY_DETAIL_CONTENT.itemTypeSingular}`}
themeColorClass="slate"
/>
</div>
{relatedStories.length > 0 && (
<div className="mt-12 md:mt-16">
<div className="flex justify-between items-baseline mb-8">
<h2 className="font-quicksand text-3xl font-bold text-slate-800">
{BUSINESS_STORY_DETAIL_CONTENT.keepReadingTitle || "Другие кейсы"}
</h2>
<a
href={`#${SECTION_IDS.businessLandingPage}`}
onClick={handleShowAllKeepReading}
className="group inline-flex items-center text-base font-medium text-slate-700 hover:text-black transition-colors whitespace-nowrap"
aria-label={`Посмотреть все ${BUSINESS_STORY_DETAIL_CONTENT.itemTypePlural}`}
>
{BUSINESS_STORY_DETAIL_CONTENT.viewAllButtonText || `Все ${BUSINESS_STORY_DETAIL_CONTENT.itemTypePlural}`}
<ChevronRightIcon className="ml-1 w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" />
</a>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{relatedStories.map(relatedStory => (
<BusinessCard
key={relatedStory.id}
story={relatedStory}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
/>
))}
</div>
</div>
)}
</div>
</section>
);
};
export default BusinessStoryDetail;

47
components/Button.tsx Executable file
View File

@@ -0,0 +1,47 @@
import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'outline' | 'secondary';
size?: 'sm' | 'md' | 'lg';
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
children: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
leftIcon,
rightIcon,
children,
className = '',
...props
}) => {
const baseStyles = "inline-flex items-center justify-center font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-150 ease-in-out font-quicksand disabled:opacity-50 disabled:cursor-not-allowed";
const sizeStyles = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
const variantStyles = {
primary: 'text-slate-800 bg-slate-50 hover:bg-slate-100 focus:ring-slate-300 border border-slate-200',
outline: 'text-slate-700 border border-slate-300 hover:bg-slate-100 focus:ring-slate-300',
secondary: 'text-slate-700 hover:bg-slate-100 focus:ring-slate-300',
};
return (
<button
type="button"
className={`${baseStyles} ${sizeStyles[size]} ${variantStyles[variant]} ${className}`}
{...props}
>
{leftIcon && <span className="mr-2 -ml-1 h-5 w-5">{leftIcon}</span>}
{children}
{rightIcon && <span className="ml-2 -mr-1 h-5 w-5">{rightIcon}</span>}
</button>
);
};
export default Button;

96
components/CareersSection.tsx Executable file
View File

@@ -0,0 +1,96 @@
import React from 'react';
import { SECTION_IDS, CAREERS_PAGE_CONTENT } from '../constants'; // mockVacancies removed
import Button from './Button';
import { ChevronRightIcon, BriefcaseIcon } from './icons'; // Added BriefcaseIcon
import { CurrentView, Vacancy } from '../types';
interface CareersSectionProps {
vacancies: Vacancy[]; // Changed from mockVacancies to accept prop
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const CareersSection: React.FC<CareersSectionProps> = ({ vacancies, setCurrentView, setSelectedItemId }) => {
const handleDetailsClick = (vacancyId: string) => {
setCurrentView('vacancyDetail');
setSelectedItemId(vacancyId);
};
return (
<section
id={SECTION_IDS.careersPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-12 md:mb-16 text-center md:text-left">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900">
{CAREERS_PAGE_CONTENT.title}
</h1>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-2xl mx-auto md:mx-0">
{CAREERS_PAGE_CONTENT.subtitle}
</p>
</header>
<div className="space-y-8">
{vacancies && vacancies.length > 0 ? (
vacancies.map((vacancy: Vacancy) => (
<div key={vacancy.id} className="bg-white p-6 rounded-xl">
<div className="md:flex md:justify-between md:items-start">
<div>
<h2 className="text-2xl font-quicksand font-semibold text-slate-800 mb-1">{vacancy.title}</h2>
<p className="text-sm text-slate-500 mb-2">
{vacancy.department} {vacancy.location} {vacancy.type}
</p>
<p className="text-slate-600 font-inter line-clamp-2">{vacancy.description}</p>
</div>
<div className="mt-4 md:mt-0 md:ml-6 flex-shrink-0">
<Button
variant="outline"
size="md"
onClick={() => handleDetailsClick(vacancy.id)}
rightIcon={<ChevronRightIcon />}
aria-label={`Подробнее о вакансии ${vacancy.title}`}
>
Подробнее
</Button>
</div>
</div>
</div>
))
) : (
<div className="text-center py-10 bg-white p-6 rounded-xl">
<BriefcaseIcon className="w-16 h-16 mx-auto text-slate-300 mb-4" />
<h2 className="text-2xl font-quicksand font-semibold text-slate-700 mb-3">{CAREERS_PAGE_CONTENT.noVacanciesTitle}</h2>
<p className="text-slate-600 font-inter">
{CAREERS_PAGE_CONTENT.noVacanciesMessagePt1}
</p>
<p className="text-slate-600 font-inter mt-3">
Вы можете следить за обновлениями на этой странице или отправить свое резюме и сопроводительное письмо на адрес <a href={`mailto:${CAREERS_PAGE_CONTENT.hrEmail}`} className="text-slate-700 hover:text-black hover:underline">{CAREERS_PAGE_CONTENT.noVacanciesContactEmailText}</a>. {CAREERS_PAGE_CONTENT.noVacanciesMessagePt2}
</p>
</div>
)}
</div>
<div className="mt-16 text-center">
<h3 className="text-xl font-quicksand font-semibold text-slate-700 mb-3">{CAREERS_PAGE_CONTENT.notFoundTitle}</h3>
<p className="text-slate-600 font-inter mb-6 max-w-xl mx-auto">
{CAREERS_PAGE_CONTENT.notFoundMessage}
</p>
<Button
variant="primary"
size="lg"
onClick={() => window.location.href = `mailto:${CAREERS_PAGE_CONTENT.hrEmail}?subject=${encodeURIComponent(CAREERS_PAGE_CONTENT.applySubject)}`}
>
{CAREERS_PAGE_CONTENT.applyButtonText}
</Button>
</div>
</div>
</section>
);
};
export default CareersSection;

194
components/ChatInput.tsx Executable file
View File

@@ -0,0 +1,194 @@
import React, { useState, useEffect, useRef } from 'react';
import { CurrentView } from '../types';
import { NAVIGABLE_VIEWS } from '../constants';
import { ArrowUpIcon } from './icons';
const PLACEHOLDERS = [
"Создайте мне сайт для моего бизнеса",
"Какие у вас есть вакансии?",
"Расскажите о ваших услугах для бизнеса",
"Мне нужно мобильное приложение",
"Что такое ваш ИИ-акселератор?",
"Я хочу научиться ИИ",
"Покажите ваши последние исследования"
];
interface ChatInputProps {
setCurrentView: (view: CurrentView) => void;
isSticky?: boolean;
autoFocusOnMount?: boolean;
}
const ChatInput: React.FC<ChatInputProps> = ({ setCurrentView, isSticky = false, autoFocusOnMount = false }) => {
const [prompt, setPrompt] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [currentPlaceholderIndex, setCurrentPlaceholderIndex] = useState(0);
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
const interval = setInterval(() => {
setCurrentPlaceholderIndex((prevIndex) => (prevIndex + 1) % PLACEHOLDERS.length);
}, 5000); // Change placeholder every 5 seconds
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}
}, [prompt]);
useEffect(() => {
if (autoFocusOnMount && textareaRef.current) {
// Small timeout to ensure the element is fully visible and transitions are complete before focusing
const timer = setTimeout(() => {
textareaRef.current?.focus();
}, 100);
return () => clearTimeout(timer);
}
}, [autoFocusOnMount]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!prompt.trim() || isLoading) return;
setIsLoading(true);
setError(null);
const validViewsString = NAVIGABLE_VIEWS.join(', ');
const systemInstruction = `You are an expert navigation assistant for a website. Your task is to analyze the user's query and determine which section of the website they want to visit. Respond ONLY with a JSON object containing a single key "view". The possible values for "view" are: ${validViewsString}.
Here are some examples of user queries and your expected JSON response:
- "I want to see your work" -> {"view": "storiesAll"}
- "Do you have any jobs?" -> {"view": "careers"}
- "What services do you offer for businesses?" -> {"view": "businessServices"}
- "I need a website" or "I need a program" -> {"view": "businessServices"}
- "Tell me about your company" -> {"view": "about"}
- "How can I contact you?" -> {"view": "main"}
- "What AI research do you do?" -> {"view": "researchAll"}
- "I want to learn AI" -> {"view": "educationStudents"}
- "I have an idea" or "I have a project" -> {"view": "acceleratorAbout"}
- "Do you have an accelerator?" -> {"view": "acceleratorAbout"}
If the user's query is about having an idea or a project they want to develop, direct them to the accelerator. If they explicitly want to order a service like a website or an app, direct them to business services.
If the user's query is unclear or doesn't map to a specific view, respond with {"view": "main"}.
Your entire response must be only the JSON object, with no other text, explanation, or markdown formatting.`;
try {
const response = await fetch('https://ai.iieasy.ru/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'local-model',
messages: [
{ role: 'system', content: systemInstruction },
{ role: 'user', content: prompt }
],
temperature: 0.1,
}),
});
if (!response.ok) {
throw new Error(`Ошибка сети: ${response.status} ${response.statusText}. Убедитесь, что сервер LM Studio запущен.`);
}
const data = await response.json();
if (data.error) {
throw new Error(`LM Studio вернула ошибку: ${data.error.message}`);
}
const responseJsonString = data.choices?.[0]?.message?.content;
if (!responseJsonString) {
throw new Error("LM Studio вернула пустой ответ.");
}
// More robust JSON extraction
let parsedData = null;
const jsonMatch = responseJsonString.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
parsedData = JSON.parse(jsonMatch[0]);
} catch (e) {
// JSON might be invalid, fall through to error
console.error("Failed to parse JSON from response:", e);
}
}
if (parsedData && parsedData.view && NAVIGABLE_VIEWS.includes(parsedData.view as any)) {
setCurrentView(parsedData.view);
} else {
setError("К сожалению, я не смог понять ваш запрос. Попробуйте переформулировать.");
}
} catch (err: any) {
console.error("Ошибка при обращении к LM Studio:", err);
setError(err.message || "Произошла ошибка. Убедитесь, что сервер LM Studio запущен и модель загружена. Пожалуйста, попробуйте еще раз.");
} finally {
setIsLoading(false);
}
};
const wrapperClasses = isSticky ? "" : "relative z-10 mx-auto w-full max-w-3xl";
const baseLabelClasses = "relative flex w-full cursor-text flex-col overflow-hidden rounded-3xl py-3 pl-4 pr-14 transition-all duration-300";
const nonStickyLabelClasses = "border border-slate-200 bg-white shadow-sm focus-within:shadow-md";
const stickyLabelClasses = "border border-slate-300 bg-white/80 shadow-lg backdrop-blur-xl focus-within:shadow-xl";
const labelClasses = `${baseLabelClasses} ${isSticky ? stickyLabelClasses : nonStickyLabelClasses}`;
return (
<div className={wrapperClasses}>
<form onSubmit={handleSubmit} className="relative">
<label className={labelClasses}>
<div className={`text-slate-400 min-h-[24px] pointer-events-none absolute left-0 top-0 w-full select-none px-4 pt-[18px] text-base transition-opacity duration-200 ${prompt ? 'opacity-0' : 'opacity-100'}`} aria-hidden={!!prompt}>
<div className="transition-transform ease-in-out duration-300" style={{ transform: `translateY(-${currentPlaceholderIndex * 1.5}rem)` }}>
{PLACEHOLDERS.map((text, index) => (
<div key={index} className={`overflow-hidden text-ellipsis whitespace-nowrap transition-opacity duration-300 ${currentPlaceholderIndex === index ? 'opacity-100' : 'opacity-0'}`}>
{text}
</div>
))}
</div>
</div>
<textarea
ref={textareaRef}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) handleSubmit(e); }}
className="w-full resize-none bg-transparent text-base focus:outline-none text-slate-800"
rows={1}
style={{ minHeight: '24px' }}
aria-label="Задайте ваш вопрос"
/>
</label>
<div className="absolute bottom-2 right-2 mt-auto flex justify-end">
<button
className="bg-slate-800 text-white disabled:bg-slate-200 disabled:text-slate-400 relative h-10 w-10 rounded-full p-0 transition-colors hover:bg-black disabled:hover:bg-slate-200 flex items-center justify-center"
type="submit"
disabled={!prompt.trim() || isLoading}
aria-label="Отправить запрос"
>
{isLoading ? (
<svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<ArrowUpIcon className="w-5 h-5" />
)}
</button>
</div>
</form>
{error && <p className="mt-2 text-center text-sm text-red-600">{error}</p>}
</div>
);
};
export default ChatInput;

40
components/ConnectSection.tsx Executable file
View File

@@ -0,0 +1,40 @@
import React from 'react';
import { SECTION_IDS, CONNECT_SECTION_CONTENT } from '../constants';
// Icons are now part of CONNECT_SECTION_CONTENT.contactMethods
const ConnectSection: React.FC = () => {
return (
<section
id={SECTION_IDS.connect}
className="py-20 md:py-24 bg-white"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto bg-white rounded-xl p-8 md:p-12 text-center transform transition-all">
<h2 className="font-quicksand text-4xl sm:text-5xl font-bold text-slate-900 mb-4">
{CONNECT_SECTION_CONTENT.title}
</h2>
<p className="font-inter text-lg text-slate-600 mb-10 md:mb-12">
{CONNECT_SECTION_CONTENT.subtitle}
</p>
<div className="flex flex-col sm:flex-row flex-wrap justify-center items-center gap-4 sm:gap-6">
{CONNECT_SECTION_CONTENT.contactMethods.map((method) => (
<a
key={method.id}
href={method.href}
target="_blank"
rel="noopener noreferrer"
className={`group inline-flex items-center justify-center px-6 py-3 text-base font-medium text-slate-800 ${method.bgColor} ${method.hoverBgColor} rounded-full focus:outline-none focus:ring-2 ${method.ringColor} focus:ring-offset-2 transition-all duration-150 ease-in-out font-quicksand w-full sm:w-auto sm:min-w-[180px]`}
aria-label={method.ariaLabel}
>
<method.icon className="w-5 h-5" />
<span className="ml-2.5">{method.name}</span>
</a>
))}
</div>
</div>
</div>
</section>
);
};
export default ConnectSection;

1080
components/ConstantsEditorPage.tsx Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,184 @@
import React from 'react';
import { SECTION_IDS, EDUCATION_BUSINESS_CONTENT } from '../constants';
import { BusinessCourse } from '../types';
import {
CpuChipIcon, ChartBarIcon, CommandLineIcon, AcademicCapIcon, LightBulbIcon, CheckIcon
} from '../components/icons';
import ApproachCard from './ApproachCard';
// Icon map for Business Courses
const businessCourseIconMap: Record<string, React.ElementType> = {
CpuChipIcon,
ChartBarIcon,
CommandLineIcon,
AcademicCapIcon,
LightBulbIcon,
};
interface CourseHighlightProps {
iconName: string;
title: string;
description: string;
}
const CourseHighlight: React.FC<CourseHighlightProps> = ({ iconName, title, description }) => {
const IconComponent = businessCourseIconMap[iconName] || AcademicCapIcon; // Fallback icon
return (
<div className="bg-white p-6 rounded-xl">
<div className="flex items-start">
<div className="flex-shrink-0">
<IconComponent className="w-8 h-8 text-slate-600" aria-hidden="true" />
</div>
<div className="ml-4">
<h3 className="font-quicksand text-xl font-semibold text-slate-800 mb-2">{title}</h3>
<p className="font-inter text-slate-600 leading-relaxed">
{description}
</p>
</div>
</div>
</div>
);
};
const PersonIcon = ({ color }: { color: string }) => (
<div className="relative w-12 h-12">
<div className={`w-8 h-8 ${color} rounded-full absolute top-0 left-1/2 -translate-x-1/2`}></div>
<div className={`w-12 h-6 ${color} rounded-t-lg absolute bottom-0`}></div>
</div>
);
interface EducationBusinessSectionProps {
businessCourses: BusinessCourse[];
}
const EducationBusinessSection: React.FC<EducationBusinessSectionProps> = ({ businessCourses }) => {
return (
<section
id={SECTION_IDS.educationBusinessPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-12 md:mb-16">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900">
{EDUCATION_BUSINESS_CONTENT.title}
</h1>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-3xl">
{EDUCATION_BUSINESS_CONTENT.subtitle}
</p>
</header>
{/* "Our Approach" section */}
<div className="my-16 md:my-24">
<div className="max-w-3xl mx-auto text-center mb-12">
<h2 className="text-3xl sm:text-4xl font-bold font-quicksand text-slate-800">Наш подход к обучению</h2>
<p className="mt-4 text-lg text-slate-600 font-inter">
Мы верим в комплексный подход, который готовит специалистов к реальным вызовам в области ИИ.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<ApproachCard
title="Обучаем"
description="Мы начинаем с фундаментальных принципов работы с ИИ, отсеивая сложную теорию и фокусируясь на практическом применении для решения реальных задач."
visual={
<div className="relative w-40 h-24 flex items-center justify-center">
<div className="absolute top-0 left-0 w-20 h-20 bg-black rounded-full"></div>
<div className="absolute bottom-0 right-0 w-20 h-20 bg-gray-300 rounded-full"></div>
<div className="absolute bottom-4 right-4 w-8 h-8 bg-green-400 rounded-lg flex items-center justify-center ring-4 ring-white">
<CheckIcon className="w-5 h-5 text-white" strokeWidth={2.5} />
</div>
</div>
}
/>
<ApproachCard
title="Тестируем"
description="Мы проводим внутренние оценки и работаем с экспертами для проверки знаний в реальных сценариях, что помогает закрепить навыки и подготовиться к сертификации."
visual={
<div className="relative w-40 h-24 flex items-center justify-center">
<div className="relative w-36 h-16 bg-gray-100 rounded-lg p-3 space-y-2 flex flex-col justify-center">
<div className="w-full h-1.5 bg-gray-300 rounded-full"></div>
<div className="w-2/3 h-1.5 bg-gray-300 rounded-full"></div>
</div>
<div className="absolute top-4 left-4 w-8 h-8 bg-green-400 rounded-lg flex items-center justify-center ring-4 ring-white">
<CheckIcon className="w-5 h-5 text-white" strokeWidth={2.5} />
</div>
</div>
}
/>
<ApproachCard
title="Делимся опытом"
description="Мы используем обратную связь из реального мира и опыт наших экспертов, чтобы сделать наши курсы по ИИ более понятными, актуальными и полезными для людей."
visual={
<div className="w-32 h-32 grid grid-cols-2 gap-2 content-center justify-items-center">
<PersonIcon color="bg-gray-300" />
<PersonIcon color="bg-black" />
<PersonIcon color="bg-gray-300" />
<div className="relative">
<PersonIcon color="bg-black" />
<div className="absolute bottom-1 right-0 w-6 h-6 bg-green-400 rounded-lg flex items-center justify-center ring-2 ring-white">
<CheckIcon className="w-4 h-4 text-white" strokeWidth={2.5} />
</div>
</div>
</div>
}
/>
</div>
</div>
<div className="my-16 md:my-20">
<h2 className="text-center font-quicksand text-3xl sm:text-4xl font-semibold text-slate-700 mb-10">
Курсы для бизнеса
</h2>
{businessCourses && businessCourses.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{businessCourses.map((course: BusinessCourse) => (
<CourseHighlight
key={course.title}
iconName={course.icon}
title={course.title}
description={course.description}
/>
))}
</div>
) : (
<div className="text-center py-10 bg-white p-6 rounded-xl">
<AcademicCapIcon className="w-16 h-16 mx-auto text-slate-300 mb-4" />
<h2 className="text-2xl font-quicksand font-semibold text-slate-700 mb-3">Курсы для бизнеса скоро появятся</h2>
<p className="text-slate-600 font-inter">
Мы активно работаем над созданием актуальных программ обучения. Следите за обновлениями!
</p>
</div>
)}
</div>
<div className="mt-16 text-center bg-white p-8 md:p-12 rounded-xl">
<h2 className="text-3xl font-quicksand font-semibold text-slate-800 mb-6">
{EDUCATION_BUSINESS_CONTENT.ctaTitle}
</h2>
<p className="font-inter text-lg text-slate-600 mb-8 max-w-xl mx-auto">
{EDUCATION_BUSINESS_CONTENT.ctaSubtitle}
</p>
<div className="flex flex-col sm:flex-row flex-wrap justify-center items-center gap-4 sm:gap-6">
{EDUCATION_BUSINESS_CONTENT.contactMethods.map((method) => (
<a
key={method.id}
href={method.href}
target="_blank"
rel="noopener noreferrer"
className={`group inline-flex items-center justify-center px-6 py-3 text-base font-medium text-slate-800 ${method.bgColor} ${method.hoverBgColor} rounded-full focus:outline-none focus:ring-2 ${method.ringColor} focus:ring-offset-2 transition-all duration-150 ease-in-out font-quicksand w-full sm:w-auto sm:min-w-[180px]`}
aria-label={method.ariaLabel}
>
<method.icon className="w-5 h-5" />
<span className="ml-2.5">{method.name}</span>
</a>
))}
</div>
</div>
</div>
</section>
);
};
export default EducationBusinessSection;

View File

@@ -0,0 +1,148 @@
import React from 'react';
import { SECTION_IDS, EDUCATION_STUDENTS_CONTENT } from '../constants';
import { StudentProgram } from '../types';
import {
LightBulbIcon, CodeBracketIcon, AcademicCapIcon, UserGroupIcon, CheckIcon
} from '../components/icons';
// Icon map for Student Programs
const studentProgramIconMap: Record<string, React.ElementType> = {
LightBulbIcon,
CodeBracketIcon,
AcademicCapIcon,
UserGroupIcon, // Example
// ... other icons
};
interface ProgramHighlightProps {
iconName: string; // Changed from icon: React.ElementType
title: string;
targetAudience: string;
description: string;
}
const ProgramHighlight: React.FC<ProgramHighlightProps> = ({ iconName, title, targetAudience, description }) => {
const IconComponent = studentProgramIconMap[iconName] || AcademicCapIcon; // Fallback icon
return (
<div className="bg-white p-6 rounded-xl">
<div className="flex items-center text-slate-700 mb-4">
<IconComponent className="w-8 h-8 mr-3" />
<div>
<h3 className="font-quicksand text-2xl font-semibold text-slate-800">{title}</h3>
<p className="font-inter text-sm text-slate-500">{targetAudience}</p>
</div>
</div>
<p className="font-inter text-slate-600 leading-relaxed">
{description}
</p>
</div>
);
};
interface EducationStudentsSectionProps {
studentPrograms: StudentProgram[];
}
const EducationStudentsSection: React.FC<EducationStudentsSectionProps> = ({ studentPrograms }) => {
const internshipBenefits = [
"Участие в реальных проектах для нашего портфолио.",
"Менторство от ведущих разработчиков и исследователей ИИ.",
"Создание сильного портфолио с кейсами мирового уровня.",
"Возможность трудоустройства для лучших стажеров.",
];
return (
<section
id={SECTION_IDS.educationStudentsPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-12 md:mb-16">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900">
{EDUCATION_STUDENTS_CONTENT.title}
</h1>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-3xl">
{EDUCATION_STUDENTS_CONTENT.subtitle}
</p>
</header>
<div className="my-16 md:my-24 grid md:grid-cols-2 gap-12 md:gap-16 items-center">
<div className="prose prose-lg max-w-none font-inter text-slate-700">
<h2 className="font-quicksand text-3xl sm:text-4xl font-semibold text-slate-800 mb-4">Стажировки и Практика: Ваш Старт в Мире ИИ</h2>
<p>
Мы в iiEasy верим, что будущее создается сегодня. Поэтому мы открываем двери для талантливых и амбициозных студентов, желающих получить реальный опыт в разработке сайтов, приложений и передовых ИИ-решений. Наша программа стажировки в Уфе это не просто практика, это полное погружение в рабочие процессы ведущей технологической компании.
</p>
<ul className="mt-6 space-y-3 not-prose">
{internshipBenefits.map((benefit, index) => (
<li key={index} className="flex items-start">
<CheckIcon className="w-6 h-6 text-green-500 mr-3 mt-1 flex-shrink-0" />
<span>{benefit}</span>
</li>
))}
</ul>
</div>
<div className="grid grid-cols-2 gap-4">
<img src="https://picsum.photos/seed/iieasy-intern-coding/600/400" alt="Студент-стажер в iiEasy пишет код на ноутбуке в современном офисе в Уфе." className="rounded-lg shadow-md aspect-[3/2] object-cover" />
<img src="https://picsum.photos/seed/iieasy-intern-meeting/600/400" alt="Группа стажеров и ментор обсуждают проект у доски в переговорной комнате iiEasy." className="rounded-lg shadow-md aspect-[3/2] object-cover mt-8" />
</div>
</div>
<div className="my-16 md:my-20">
<h2 className="text-center font-quicksand text-3xl sm:text-4xl font-semibold text-slate-700 mb-10">
Наши программы
</h2>
{studentPrograms && studentPrograms.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{studentPrograms.map((program: StudentProgram) => (
<ProgramHighlight
key={program.title}
iconName={program.icon}
title={program.title}
targetAudience={program.targetAudience}
description={program.description}
/>
))}
</div>
) : (
<div className="text-center py-10 bg-white p-6 rounded-xl">
<AcademicCapIcon className="w-16 h-16 mx-auto text-slate-300 mb-4" />
<h2 className="text-2xl font-quicksand font-semibold text-slate-700 mb-3">Программы для студентов скоро будут здесь</h2>
<p className="text-slate-600 font-inter">
Мы готовим увлекательные курсы и программы. Заглядывайте позже!
</p>
</div>
)}
</div>
<div className="mt-16 text-center bg-white p-8 md:p-12 rounded-xl">
<h2 className="text-3xl font-quicksand font-semibold text-slate-800 mb-6">
{EDUCATION_STUDENTS_CONTENT.ctaTitle}
</h2>
<p className="font-inter text-lg text-slate-600 mb-8 max-w-xl mx-auto">
{EDUCATION_STUDENTS_CONTENT.ctaSubtitle}
</p>
<div className="flex flex-col sm:flex-row flex-wrap justify-center items-center gap-4 sm:gap-6">
{EDUCATION_STUDENTS_CONTENT.contactMethods.map((method) => (
<a
key={method.id}
href={method.href}
target="_blank"
rel="noopener noreferrer"
className={`group inline-flex items-center justify-center px-6 py-3 text-base font-medium text-slate-800 ${method.bgColor} ${method.hoverBgColor} rounded-full focus:outline-none focus:ring-2 ${method.ringColor} focus:ring-offset-2 transition-all duration-150 ease-in-out font-quicksand w-full sm:w-auto sm:min-w-[180px]`}
aria-label={method.ariaLabel}
>
<method.icon className="w-5 h-5" />
<span className="ml-2.5">{method.name}</span>
</a>
))}
</div>
</div>
</div>
</section>
);
};
export default EducationStudentsSection;

185
components/FilterSortBar.tsx Executable file
View File

@@ -0,0 +1,185 @@
import React, { useState, useRef, useEffect } from 'react';
import { AdjustmentsHorizontalIcon, ChevronDownIcon, Squares2X2Icon, ListBulletIcon, CheckIcon } from './icons';
export type ViewMode = 'grid' | 'list';
export type SortOptionKey = 'newest' | 'oldest' | 'alphabetical';
export interface SortOption {
key: SortOptionKey;
label: string;
}
interface FilterSortBarProps {
viewMode: ViewMode;
onViewChange: (view: ViewMode) => void;
availableCategories: string[];
selectedCategories: string[];
onCategoryChange: (category: string) => void;
onClearCategories: () => void;
sortOptions: SortOption[];
selectedSort: SortOptionKey;
onSortChange: (sort: SortOptionKey) => void;
}
const Dropdown: React.FC<{
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
align?: 'left' | 'right';
className?: string;
}> = ({ isOpen, onClose, children, align = 'left', className = '' }) => {
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
onClose();
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
const alignmentClass = align === 'left' ? 'left-0' : 'right-0';
return (
<div
ref={dropdownRef}
className={`absolute top-full mt-2 w-64 rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 z-20 ${alignmentClass} ${className}`}
>
<div className="py-1">
{children}
</div>
</div>
);
};
const FilterSortBar: React.FC<FilterSortBarProps> = ({
viewMode,
onViewChange,
availableCategories,
selectedCategories,
onCategoryChange,
onClearCategories,
sortOptions,
selectedSort,
onSortChange
}) => {
const [isFilterOpen, setFilterOpen] = useState(false);
const [isSortOpen, setSortOpen] = useState(false);
const selectedSortLabel = sortOptions.find(o => o.key === selectedSort)?.label || 'Сортировка';
return (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 py-4 mb-8 border-t border-b border-slate-200">
<div className="flex items-center gap-1 sm:gap-2">
<div className="relative">
<button
type="button"
onClick={() => setFilterOpen(prev => !prev)}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-slate-700 hover:bg-slate-100 rounded-full transition-colors"
>
<AdjustmentsHorizontalIcon className="h-5 w-5 text-slate-500" />
<span>Фильтры</span>
{selectedCategories.length > 0 && <span className="bg-slate-800 text-white text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center">{selectedCategories.length}</span>}
</button>
<Dropdown isOpen={isFilterOpen} onClose={() => setFilterOpen(false)}>
<div className="px-4 py-2 flex justify-between items-center border-b border-slate-100">
<h4 className="font-semibold text-slate-800">Категории</h4>
{selectedCategories.length > 0 && (
<button
onClick={() => { onClearCategories(); setFilterOpen(false); }}
className="text-xs text-slate-600 hover:text-black hover:underline"
>
Сбросить
</button>
)}
</div>
<div className="max-h-60 overflow-y-auto no-scrollbar">
{availableCategories.length > 0 ? availableCategories.map(category => (
<label key={category} className="flex items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-50 cursor-pointer">
<input
type="checkbox"
checked={selectedCategories.includes(category)}
onChange={() => onCategoryChange(category)}
className="h-4 w-4 rounded border-slate-300 text-slate-600 focus:ring-slate-500"
/>
<span className="ml-3">{category}</span>
</label>
)) : (
<p className="px-4 py-2 text-sm text-slate-500">Нет доступных категорий.</p>
)}
</div>
</Dropdown>
</div>
<div className="relative">
<button
type="button"
onClick={() => setSortOpen(prev => !prev)}
className="flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-700 hover:bg-slate-100 rounded-full transition-colors"
>
<span>{selectedSortLabel}</span>
<ChevronDownIcon className="h-5 w-5 text-slate-500" />
</button>
<Dropdown isOpen={isSortOpen} onClose={() => setSortOpen(false)}>
{sortOptions.map(option => (
<button
key={option.key}
onClick={() => { onSortChange(option.key); setSortOpen(false); }}
className={`w-full text-left flex items-center px-4 py-2 text-sm transition-colors ${selectedSort === option.key ? 'font-semibold text-slate-900 bg-slate-50' : 'text-slate-700 hover:bg-slate-50'}`}
>
{option.label}
{selectedSort === option.key && <CheckIcon className="ml-auto h-4 w-4" />}
</button>
))}
</Dropdown>
</div>
</div>
<div className="flex items-center ml-auto gap-1 bg-slate-100 p-1 rounded-full shrink-0">
<label htmlFor="grid-view" className="cursor-pointer" aria-label="Вид сеткой">
<input
id="grid-view"
type="radio"
name="view-toggle"
value="grid"
checked={viewMode === 'grid'}
onChange={() => onViewChange('grid')}
className="sr-only peer"
/>
<div className="p-1.5 rounded-full text-slate-600 peer-checked:bg-white peer-checked:text-slate-900 peer-checked:shadow-sm hover:bg-slate-200 transition-colors">
<Squares2X2Icon className="h-5 w-5" />
</div>
</label>
<label htmlFor="list-view" className="cursor-pointer" aria-label="Вид списком">
<input
id="list-view"
type="radio"
name="view-toggle"
value="list"
checked={viewMode === 'list'}
onChange={() => onViewChange('list')}
className="sr-only peer"
/>
<div className="p-1.5 rounded-full text-slate-600 peer-checked:bg-white peer-checked:text-slate-900 peer-checked:shadow-sm hover:bg-slate-200 transition-colors">
<ListBulletIcon className="h-5 w-5" />
</div>
</label>
</div>
</div>
);
};
export default FilterSortBar;

62
components/GalleryComponent.tsx Executable file
View File

@@ -0,0 +1,62 @@
import React from 'react';
import { GalleryItem } from '../types';
import { PhotoIcon, VideoCameraIcon } from './icons'; // Assuming these exist or will be added
interface GalleryComponentProps {
items: GalleryItem[] | undefined;
title?: string;
}
const GalleryComponent: React.FC<GalleryComponentProps> = ({ items, title }) => {
if (!items || items.length === 0) {
return null;
}
return (
<div className="py-8 md:py-12">
{title && (
<h2 className="font-quicksand text-2xl sm:text-3xl font-bold text-slate-800 mb-6 md:mb-8 text-center">
{title}
</h2>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
{items.map((item) => (
<div key={item.id} className="group relative bg-slate-100 rounded-lg overflow-hidden">
<div className="aspect-[16/9] w-full">
{item.type === 'image' ? (
<img
src={item.url}
alt={item.altText || item.caption || 'Gallery image'}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy"
/>
) : item.type === 'video' ? (
<video
src={item.url}
controls
className="w-full h-full object-cover"
poster={item.altText} // Use altText as poster if available
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-slate-200">
<PhotoIcon className="w-16 h-16 text-slate-400" /> {/* Fallback icon */}
</div>
)}
</div>
{item.caption && (
<div className="absolute bottom-0 left-0 right-0 p-3 bg-gradient-to-t from-black/70 to-transparent transition-opacity duration-300 opacity-0 group-hover:opacity-100">
<p className="text-white text-sm font-inter leading-snug line-clamp-2">{item.caption}</p>
</div>
)}
{/* Icon overlay for type indication, optional */}
<div className="absolute top-2 right-2 p-1.5 bg-black/40 rounded-full text-white opacity-80 group-hover:opacity-100 transition-opacity duration-300">
{item.type === 'image' ? <PhotoIcon className="w-4 h-4" /> : <VideoCameraIcon className="w-4 h-4" />}
</div>
</div>
))}
</div>
</div>
);
};
export default GalleryComponent;

43
components/HeroSection.tsx Executable file
View File

@@ -0,0 +1,43 @@
import React from 'react';
import ChatInput from './ChatInput';
import { CurrentView } from '../types';
import { ArrowDownTrayIcon } from './icons';
import { SECTION_IDS } from '../constants';
interface HeroSectionProps {
setCurrentView: (view: CurrentView) => void;
isChatHidden?: boolean;
}
const HeroSection: React.FC<HeroSectionProps> = ({ setCurrentView, isChatHidden = false }) => {
return (
<section
id="hero"
className="relative min-h-screen flex items-center justify-center overflow-hidden bg-white p-4"
>
<div className="w-full max-w-3xl text-center">
<h1 className="mb-6 mx-auto text-4xl sm:text-5xl font-semibold leading-tight tracking-tight text-slate-900 font-quicksand">
Чем мы можем помочь?
</h1>
<div className={`transition-opacity duration-300 ${isChatHidden ? 'opacity-0' : 'opacity-100'}`}>
<ChatInput setCurrentView={setCurrentView} />
</div>
</div>
<a
href={`#${SECTION_IDS.story}`}
onClick={(e) => {
e.preventDefault();
document.getElementById(SECTION_IDS.story)?.scrollIntoView({ behavior: 'smooth' });
}}
className="absolute bottom-8 left-1/2 -translate-x-1/2 z-10"
aria-label="Прокрутить вниз"
>
<ArrowDownTrayIcon className="h-8 w-8 text-slate-500 animate-bounce hover:text-slate-700 transition-colors" />
</a>
</section>
);
};
export default HeroSection;

View File

@@ -0,0 +1,90 @@
import React from 'react';
import Button from './Button';
import { ArrowLeftIcon, ArrowRightIcon } from './icons'; // Assuming these are added to icons.tsx
interface NavigableItem {
id: string;
title: string;
}
interface ItemDetailNavigationProps {
previousItem?: NavigableItem;
nextItem?: NavigableItem;
onNavigate: (itemId: string) => void;
previousItemButtonLabel: string;
nextItemButtonLabel: string;
noPreviousItemText: string;
noNextItemText: string;
themeColorClass?: 'blue' | 'teal' | 'indigo' | 'amber' | 'slate';
}
const ItemDetailNavigation: React.FC<ItemDetailNavigationProps> = ({
previousItem,
nextItem,
onNavigate,
previousItemButtonLabel,
nextItemButtonLabel,
noPreviousItemText,
noNextItemText,
themeColorClass = 'slate', // Default theme
}) => {
const themeStyles: Record<string, string> = {
blue: 'border-blue-500 text-blue-600 hover:bg-blue-50',
teal: 'border-teal-500 text-teal-600 hover:bg-teal-50',
indigo: 'border-indigo-500 text-indigo-600 hover:bg-indigo-50',
amber: 'border-amber-500 text-amber-600 hover:bg-amber-50',
slate: 'border-slate-400 text-slate-700 hover:bg-slate-100',
}
const buttonBaseClasses = themeStyles[themeColorClass] || themeStyles.slate;
const disabledClasses = "opacity-50 cursor-not-allowed";
return (
<div className="flex justify-between items-center py-8 my-8 border-t border-b border-slate-200">
{previousItem ? (
<Button
variant="outline"
size="md"
onClick={() => onNavigate(previousItem.id)}
leftIcon={<ArrowLeftIcon className="w-5 h-5" />}
className={buttonBaseClasses}
aria-label={`${previousItemButtonLabel}: ${previousItem.title}`}
>
<div className="text-left">
<span className="block text-xs uppercase tracking-wider opacity-75">{previousItemButtonLabel}</span>
<span className="block font-semibold truncate max-w-[150px] sm:max-w-[200px] md:max-w-[250px]">{previousItem.title}</span>
</div>
</Button>
) : (
<div className={`flex items-center text-sm text-slate-500 ${disabledClasses}`}>
<ArrowLeftIcon className="w-5 h-5 mr-2" />
{noPreviousItemText}
</div>
)}
{nextItem ? (
<Button
variant="outline"
size="md"
onClick={() => onNavigate(nextItem.id)}
rightIcon={<ArrowRightIcon className="w-5 h-5" />}
className={buttonBaseClasses}
aria-label={`${nextItemButtonLabel}: ${nextItem.title}`}
>
<div className="text-right">
<span className="block text-xs uppercase tracking-wider opacity-75">{nextItemButtonLabel}</span>
<span className="block font-semibold truncate max-w-[150px] sm:max-w-[200px] md:max-w-[250px]">{nextItem.title}</span>
</div>
</Button>
) : (
<div className={`flex items-center text-sm text-slate-500 ${disabledClasses}`}>
{noNextItemText}
<ArrowRightIcon className="w-5 h-5 ml-2" />
</div>
)}
</div>
);
};
export default ItemDetailNavigation;

27
components/Logo.tsx Executable file
View File

@@ -0,0 +1,27 @@
import React from 'react';
interface LogoProps {
width?: number;
height?: number;
className?: string;
}
const Logo: React.FC<LogoProps> = ({ width = 200, height = 200, className = '' }) => {
return (
<svg
width={width}
height={height}
viewBox="0 0 200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-label="iiEasy Logo"
>
<circle cx="100" cy="100" r="70" stroke="#1F2937" strokeWidth="6" fill="none" strokeDasharray="15 85" transform="rotate(-90 100 100)"/>
<circle cx="100" cy="100" r="50" stroke="#1F2937" strokeWidth="6" fill="none" strokeDasharray="12 58" transform="rotate(-60 100 100)"/>
<circle cx="100" cy="100" r="30" stroke="#1F2937" strokeWidth="6" fill="none" strokeDasharray="8 32" transform="rotate(-30 100 100)"/>
</svg>
);
};
export default Logo;

149
components/NewsAllSection.tsx Executable file
View File

@@ -0,0 +1,149 @@
import React, { useState, useMemo } from 'react';
import { CurrentView, NewsArticle } from '../types';
import { SECTION_IDS, NEWS_ALL_CONTENT } from '../constants';
import Button from './Button';
import { ArrowUturnLeftIcon } from './icons';
import NewsCard from './NewsCard';
import FilterSortBar, { ViewMode, SortOptionKey, SortOption } from './FilterSortBar';
interface NewsAllSectionProps {
newsArticles: NewsArticle[];
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const sortOptions: SortOption[] = [
{ key: 'newest', label: 'Сначала новые' },
{ key: 'oldest', label: 'Сначала старые' },
{ key: 'alphabetical', label: 'По алфавиту (А-Я)' },
];
const NewsAllSection: React.FC<NewsAllSectionProps> = ({ newsArticles, setCurrentView, setSelectedItemId }) => {
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedSort, setSelectedSort] = useState<SortOptionKey>('newest');
const handleBackClick = () => {
setCurrentView('main');
setSelectedItemId(null);
setTimeout(() => {
document.getElementById(SECTION_IDS.latestNews)?.scrollIntoView({ behavior: 'smooth' });
}, 0);
};
const availableCategories = useMemo(() => {
if (!newsArticles) return [];
return Array.from(new Set(newsArticles.map(p => p.category))).sort();
}, [newsArticles]);
const handleCategoryChange = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const filteredAndSortedArticles = useMemo(() => {
if (!newsArticles) return [];
const filtered = selectedCategories.length > 0
? newsArticles.filter(p => selectedCategories.includes(p.category))
: newsArticles;
return [...filtered].sort((a, b) => {
switch (selectedSort) {
case 'newest':
return new Date(b.publishedAt || 0).getTime() - new Date(a.publishedAt || 0).getTime();
case 'oldest':
return new Date(a.publishedAt || 0).getTime() - new Date(b.publishedAt || 0).getTime();
case 'alphabetical':
return a.title.localeCompare(b.title);
default:
return 0;
}
});
}, [newsArticles, selectedCategories, selectedSort]);
if (!newsArticles) {
return (
<section
id={SECTION_IDS.newsAllPage}
className="py-16 md:py-24 bg-white min-h-screen flex justify-center items-center"
>
<p className="text-slate-600 font-inter">Загрузка новостей...</p>
</section>
);
}
const layoutClasses = viewMode === 'grid'
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
: "flex flex-col gap-4";
return (
<section
id={SECTION_IDS.newsAllPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-8 md:mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900 mb-4 sm:mb-0">
{NEWS_ALL_CONTENT.title}
</h1>
<Button
variant="outline"
size="md"
onClick={handleBackClick}
leftIcon={<ArrowUturnLeftIcon />}
className="self-start sm:self-center"
>
{NEWS_ALL_CONTENT.backButtonTextPrefix} {NEWS_ALL_CONTENT.backButtonTextSuffix}
</Button>
</div>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-3xl">
{NEWS_ALL_CONTENT.subtitle}
</p>
</header>
<FilterSortBar
viewMode={viewMode}
onViewChange={setViewMode}
availableCategories={availableCategories}
selectedCategories={selectedCategories}
onCategoryChange={handleCategoryChange}
onClearCategories={() => setSelectedCategories([])}
sortOptions={sortOptions}
selectedSort={selectedSort}
onSortChange={setSelectedSort}
/>
{filteredAndSortedArticles.length > 0 ? (
<div className={layoutClasses}>
{filteredAndSortedArticles.map(article => (
<NewsCard
key={article.id}
article={article}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
viewMode={viewMode}
/>
))}
</div>
) : (
<div className="text-center py-10">
<h2 className="text-2xl font-quicksand font-semibold text-slate-700 mb-3">{NEWS_ALL_CONTENT.emptyStateTitle}</h2>
<p className="text-slate-600 font-inter">
{NEWS_ALL_CONTENT.emptyStateMessage}
</p>
</div>
)}
</div>
</section>
);
};
export default NewsAllSection;

148
components/NewsArticleDetail.tsx Executable file
View File

@@ -0,0 +1,148 @@
import React from 'react';
import { CurrentView, NewsArticle } from '../types';
import { SECTION_IDS, NEWS_ARTICLE_DETAIL_CONTENT } from '../constants'; // Removed mockNews
import Button from './Button';
import { ArrowUturnLeftIcon, ChevronRightIcon } from './icons';
import ItemDetailNavigation from './ItemDetailNavigation';
import NewsCard from './NewsCard';
import GalleryComponent from './GalleryComponent'; // Import GalleryComponent
interface NewsArticleDetailProps {
itemId: string;
allNews: NewsArticle[];
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const NewsArticleDetail: React.FC<NewsArticleDetailProps> = ({ itemId, allNews, setCurrentView, setSelectedItemId }) => {
const article = allNews.find(p => p.id === itemId);
const handleBackClick = () => {
setCurrentView('newsAll');
setSelectedItemId(null);
};
const handleNavigateItem = (newItemId: string) => {
setSelectedItemId(newItemId);
};
const handleShowAllKeepReading = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
setCurrentView('newsAll');
setSelectedItemId(null);
};
if (!article) {
return (
<section id={SECTION_IDS.newsArticleDetailPage} className="py-16 md:py-24 min-h-screen flex items-center justify-center">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="text-2xl font-semibold text-slate-700">{NEWS_ARTICLE_DETAIL_CONTENT.notFoundTitle}</h1>
<Button onClick={handleBackClick} leftIcon={<ArrowUturnLeftIcon />} className="mt-8">
{NEWS_ARTICLE_DETAIL_CONTENT.backButtonText}
</Button>
</div>
</section>
);
}
const currentIndex = allNews.findIndex(p => p.id === itemId);
const previousArticle = currentIndex > 0 ? allNews[currentIndex - 1] : undefined;
const nextArticle = currentIndex < allNews.length - 1 ? allNews[currentIndex + 1] : undefined;
const relatedArticles = allNews
.filter(p => p.id !== itemId)
.slice(0, 3);
return (
<section
id={SECTION_IDS.newsArticleDetailPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<Button
variant="outline"
size="sm"
onClick={handleBackClick}
leftIcon={<ArrowUturnLeftIcon />}
className="mb-6"
>
{NEWS_ARTICLE_DETAIL_CONTENT.backButtonText}
</Button>
{article.imageUrl && (
<div className="mb-8 rounded-lg overflow-hidden">
<img src={article.imageUrl} alt={article.title} className="w-full h-auto object-cover max-h-[50vh]" />
</div>
)}
<div className="max-w-3xl mx-auto">
<header className="mb-8 md:mb-12">
<h1 className="font-quicksand text-3xl sm:text-4xl md:text-5xl font-bold text-slate-800 mb-3">
{article.title}
</h1>
<div className="font-inter text-sm text-slate-500">
<span className="font-semibold text-slate-600">{article.category}</span>
<span className="mx-1.5"></span>
<span>{article.date}</span>
</div>
</header>
<article className="prose prose-lg max-w-none font-inter text-slate-700">
{article.description && <p className="lead text-xl">{article.description}</p>}
{typeof article.fullContent === 'string' ? (
<div dangerouslySetInnerHTML={{ __html: article.fullContent }} />
) : (
article.fullContent
)}
</article>
{article.gallery && article.gallery.length > 0 && (
<GalleryComponent items={article.gallery} title={NEWS_ARTICLE_DETAIL_CONTENT.galleryTitle} />
)}
<ItemDetailNavigation
previousItem={previousArticle ? { id: previousArticle.id, title: previousArticle.title } : undefined}
nextItem={nextArticle ? { id: nextArticle.id, title: nextArticle.title } : undefined}
onNavigate={handleNavigateItem}
previousItemButtonLabel={NEWS_ARTICLE_DETAIL_CONTENT.previousItemButtonLabel || `Предыдущая ${NEWS_ARTICLE_DETAIL_CONTENT.itemTypeSingular}`}
nextItemButtonLabel={NEWS_ARTICLE_DETAIL_CONTENT.nextItemButtonLabel || `Следующая ${NEWS_ARTICLE_DETAIL_CONTENT.itemTypeSingular}`}
noPreviousItemText={NEWS_ARTICLE_DETAIL_CONTENT.noPreviousItemText || `Это первая ${NEWS_ARTICLE_DETAIL_CONTENT.itemTypeSingular}`}
noNextItemText={NEWS_ARTICLE_DETAIL_CONTENT.noNextItemText || `Это последняя ${NEWS_ARTICLE_DETAIL_CONTENT.itemTypeSingular}`}
themeColorClass="slate"
/>
</div>
{relatedArticles.length > 0 && (
<div className="mt-12 md:mt-16">
<div className="flex justify-between items-baseline mb-8">
<h2 className="font-quicksand text-3xl font-bold text-slate-800">
{NEWS_ARTICLE_DETAIL_CONTENT.keepReadingTitle || "Другие новости"}
</h2>
<a
href={`#${SECTION_IDS.newsAllPage}`}
onClick={handleShowAllKeepReading}
className="group inline-flex items-center text-base font-medium text-slate-700 hover:text-black transition-colors whitespace-nowrap"
aria-label={`Посмотреть все ${NEWS_ARTICLE_DETAIL_CONTENT.itemTypePlural}`}
>
{NEWS_ARTICLE_DETAIL_CONTENT.viewAllButtonText || `Все ${NEWS_ARTICLE_DETAIL_CONTENT.itemTypePlural}`}
<ChevronRightIcon className="ml-1 w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" />
</a>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
{relatedArticles.map(relatedArticle => (
<NewsCard
key={relatedArticle.id}
article={relatedArticle}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
/>
))}
</div>
</div>
)}
</div>
</section>
);
};
export default NewsArticleDetail;

87
components/NewsCard.tsx Executable file
View File

@@ -0,0 +1,87 @@
import React from 'react';
import { NewsArticle, CurrentView } from '../types';
import { ViewMode } from './FilterSortBar';
interface NewsCardProps {
article: NewsArticle;
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
viewMode?: ViewMode;
}
const NewsCard: React.FC<NewsCardProps> = ({ article, setCurrentView, setSelectedItemId, viewMode = 'grid' }) => {
const handleCardClick = () => {
setCurrentView('newsArticleDetail');
setSelectedItemId(article.id);
};
if (viewMode === 'list') {
return (
<div
onClick={handleCardClick}
className="group block p-4 rounded-lg bg-white transition-all duration-300 ease-in-out hover:bg-slate-50 cursor-pointer w-full"
role="article"
aria-labelledby={`news-title-${article.id}`}
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0 w-24 h-24 md:w-28 md:h-28 rounded-md overflow-hidden bg-slate-200">
<img
src={article.imageUrl}
alt={`Изображение для новости: ${article.title}`}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 ease-in-out"
loading="lazy"
/>
</div>
<div className="flex-1">
<h4 id={`news-title-${article.id}`} className="text-base md:text-lg font-quicksand font-semibold text-slate-800 group-hover:text-black mb-1 transition-colors duration-200 leading-tight">
{article.title}
</h4>
<p className="text-xs text-slate-500 mb-2">
<span className="font-semibold text-slate-600">{article.category}</span>
<span className="mx-1.5 text-slate-400"></span>
<span>{article.date}</span>
</p>
{article.description && (
<p className="text-sm text-slate-600 line-clamp-2 font-inter">{article.description}</p>
)}
</div>
</div>
</div>
);
}
return (
<div
onClick={handleCardClick}
className="group block rounded-xl overflow-hidden transition-all duration-300 ease-in-out bg-white transform hover:-translate-y-1 cursor-pointer shadow-sm hover:shadow-md"
role="article"
aria-labelledby={`news-title-${article.id}`}
>
<div className="relative w-full overflow-hidden aspect-video">
<img
src={article.imageUrl}
alt={`Изображение для новости: ${article.title}`}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 ease-in-out"
loading="lazy"
/>
</div>
<div className="p-4">
<p className="text-xs text-slate-500 mb-2">
<span className="font-semibold text-slate-600">{article.category}</span>
<span className="mx-1.5 text-slate-400"></span>
<span>{article.date}</span>
</p>
<h4 id={`news-title-${article.id}`} className="text-base font-quicksand font-semibold text-slate-800 group-hover:text-black mb-1 transition-colors duration-200 leading-tight">
{article.title}
</h4>
{article.description && (
<p className="text-sm text-slate-600 line-clamp-3 font-inter">{article.description}</p>
)}
</div>
</div>
);
};
export default NewsCard;

64
components/NewsSection.tsx Executable file
View File

@@ -0,0 +1,64 @@
import React from 'react';
import { CurrentView, NewsArticle } from '../types'; // Added NewsArticle type
import { SECTION_IDS, NEWS_SECTION_CONTENT } from '../constants';
import { ChevronRightIcon } from './icons';
import NewsCard from './NewsCard';
interface NewsSectionProps {
newsArticles: NewsArticle[]; // Changed from mockNews
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const NewsSection: React.FC<NewsSectionProps> = ({ newsArticles, setCurrentView, setSelectedItemId }) => {
const handleShowAllClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
setCurrentView('newsAll');
setSelectedItemId(null);
};
if (!newsArticles || newsArticles.length === 0) {
return (
<section id={SECTION_IDS.latestNews} className="py-16 md:py-24 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<p className="text-slate-600 font-inter">Загрузка новостей...</p>
</div>
</section>
);
}
return (
<section id={SECTION_IDS.latestNews} className="py-16 md:py-24 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-baseline mb-12 md:mb-16">
<h2 className="font-quicksand text-4xl sm:text-5xl font-bold text-slate-900">
{NEWS_SECTION_CONTENT.title}
</h2>
<a
href={`#${SECTION_IDS.newsAllPage}`}
onClick={handleShowAllClick}
className="group inline-flex items-center text-base font-medium text-slate-700 hover:text-black transition-colors whitespace-nowrap"
aria-label={NEWS_SECTION_CONTENT.showAllAriaLabel}
>
{NEWS_SECTION_CONTENT.showAllText}
<ChevronRightIcon className="ml-1 w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" />
</a>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
{newsArticles.slice(-4).reverse().map(article => (
<NewsCard
key={article.id}
article={article}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
/>
))}
</div>
</div>
</section>
);
};
export default NewsSection;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { SECTION_IDS, OUR_MISSION_CONTENT } from '../constants';
import { BookOpenIcon } from './icons'; // A fitting icon for a manifesto
const MissionSectionItem: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => (
<div className="mb-10">
<h2 className="font-quicksand text-2xl sm:text-3xl font-bold text-slate-800 mb-4">{title}</h2>
<div className="prose prose-lg max-w-none font-inter text-slate-700">
{typeof children === 'string' ? <p>{children}</p> : children}
</div>
</div>
);
const OurMissionSection: React.FC = () => {
return (
<section
id={SECTION_IDS.ourMissionPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-4xl">
<header className="mb-12 md:mb-16 text-center">
<BookOpenIcon className="w-16 h-16 mx-auto text-slate-400 mb-4" />
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900">
{OUR_MISSION_CONTENT.title}
</h1>
</header>
<div className="prose prose-xl max-w-none font-inter text-slate-700 mb-12">
{typeof OUR_MISSION_CONTENT.preamble === 'string'
? <p>{OUR_MISSION_CONTENT.preamble}</p>
: OUR_MISSION_CONTENT.preamble
}
</div>
<div className="mt-12">
{OUR_MISSION_CONTENT.sections.map((section, index) => (
<MissionSectionItem key={index} title={section.title}>
{section.content}
</MissionSectionItem>
))}
</div>
<footer className="mt-12 pt-8 border-t border-slate-200">
<div className="prose prose-lg max-w-none font-inter text-slate-700">
<p className="font-semibold">{OUR_MISSION_CONTENT.closingStatement}</p>
</div>
</footer>
</div>
</section>
);
};
export default OurMissionSection;

146
components/PostCard.tsx Executable file
View File

@@ -0,0 +1,146 @@
import React, { useState, useRef } from 'react';
import { Post, CurrentView } from '../types';
import { PlayIcon, PauseIcon } from './icons';
import { UI_TEXTS } from '../constants';
import { ViewMode } from './FilterSortBar';
interface PostCardProps {
post: Post;
isFeatured?: boolean;
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
viewMode?: ViewMode;
}
const PostCard: React.FC<PostCardProps> = ({ post, isFeatured = false, setCurrentView, setSelectedItemId, viewMode = 'grid' }) => {
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
const toggleVideoPlay = (e: React.MouseEvent) => {
e.stopPropagation(); // Prevent card click event when clicking video button
if (videoRef.current) {
if (videoRef.current.paused) {
videoRef.current.play();
setIsVideoPlaying(true);
} else {
videoRef.current.pause();
setIsVideoPlaying(false);
}
}
};
const handleCardClick = () => {
setCurrentView('blogPostDetail');
setSelectedItemId(post.id);
};
if (viewMode === 'list' && !isFeatured) {
return (
<div
onClick={handleCardClick}
className="group block p-4 rounded-lg bg-white transition-all duration-300 ease-in-out hover:bg-slate-50 cursor-pointer w-full"
role="article"
aria-labelledby={`post-title-${post.id}`}
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0 w-24 h-24 md:w-28 md:h-28 rounded-md overflow-hidden bg-slate-200">
<img
src={post.imageUrl}
alt={post.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 ease-in-out"
loading="lazy"
/>
</div>
<div className="flex-1">
<h3 id={`post-title-${post.id}`} className="text-base md:text-lg font-quicksand font-semibold text-slate-800 group-hover:text-black mb-1 transition-colors duration-200 leading-tight">
{post.title}
</h3>
<div className="font-inter text-xs md:text-sm text-slate-500 mb-2">
<span className="font-semibold text-slate-600">{post.category}</span>
{(post.date || post.readTime) && <span className="mx-1.5"></span>}
{post.date && <span>{post.date}</span>}
{post.date && post.readTime && <span className="mx-1.5"></span>}
{post.readTime && <span>{post.readTime}</span>}
</div>
{post.description && (
<p className="text-sm text-slate-600 line-clamp-2 font-inter">{post.description}</p>
)}
</div>
</div>
</div>
);
}
// Grid View (or Featured Card)
const imageAspectRatio = isFeatured ? "aspect-[4/5] md:aspect-[16/9]" : "aspect-[1/1]";
const titleSize = isFeatured ? "text-2xl md:text-3xl lg:text-4xl" : "text-xl md:text-lg";
const scaleEffect = isFeatured ? "group-hover:scale-[1.0125]" : "group-hover:scale-[1.025]";
return (
<div
className={`group relative rounded-xl overflow-hidden transition-all duration-300 bg-white ${isFeatured ? 'flex flex-col' : ''} cursor-pointer shadow-sm hover:shadow-md`}
onClick={handleCardClick}
role="article"
aria-labelledby={`post-title-${post.id}`}
>
<div className={`relative w-full overflow-hidden ${imageAspectRatio} ${scaleEffect} transition-transform duration-300`}>
{post.videoUrl && !isFeatured ? (
<>
<video
ref={videoRef}
loop
playsInline
muted
className="absolute inset-0 w-full h-full object-cover"
poster={post.imageUrl}
onPlay={() => setIsVideoPlaying(true)}
onPause={() => setIsVideoPlaying(false)}
onClick={(e) => e.stopPropagation()} // Prevent card click on video itself
>
<source src={post.videoUrl} type="video/mp4" />
Your browser does not support the video tag.
</video>
<button
onClick={toggleVideoPlay}
className="absolute top-2 right-2 z-10 p-2 bg-black bg-opacity-50 rounded-full text-white hover:bg-opacity-75 transition-opacity"
aria-label={isVideoPlaying ? UI_TEXTS.pauseVideoAriaLabel : UI_TEXTS.playVideoAriaLabel}
>
{isVideoPlaying ? <PauseIcon className="w-5 h-5" /> : <PlayIcon className="w-5 h-5" />}
</button>
</>
) : (
<img
src={post.imageUrl}
alt={post.title}
className="absolute inset-0 w-full h-full object-cover"
loading="lazy"
/>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</div>
<div className={`p-4 md:p-6 ${isFeatured ? 'flex-grow flex flex-col justify-between' : ''}`}>
<div>
<h3 id={`post-title-${post.id}`} className={`font-quicksand font-bold ${titleSize} mb-2 text-slate-800 transition-colors`}>
{post.title}
</h3>
{isFeatured && post.description && (
<p className="font-inter text-sm md:text-base text-slate-600 mb-3 line-clamp-3">{post.description}</p>
)}
{!isFeatured && post.description && (
<p className="font-inter text-sm text-slate-600 mb-3 line-clamp-2">{post.description}</p>
)}
</div>
<div className="font-inter text-xs md:text-sm text-slate-500">
<span className="font-semibold text-slate-600">{post.category}</span>
{(post.date || post.readTime) && <span className="mx-1.5"></span>}
{post.date && <span>{post.date}</span>}
{post.date && post.readTime && <span className="mx-1.5"></span>}
{post.readTime && <span>{post.readTime}</span>}
</div>
</div>
</div>
);
};
export default PostCard;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { CurrentView } from '../types';
import { SECTION_IDS, PRIVACY_POLICY_CONTENT } from '../constants';
import Button from './Button';
import { ArrowUturnLeftIcon } from './icons';
interface PrivacyPolicySectionProps {
setCurrentView: (view: CurrentView) => void;
}
const PrivacyPolicySection: React.FC<PrivacyPolicySectionProps> = ({ setCurrentView }) => {
const handleBackClick = () => {
setCurrentView('main');
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return (
<section
id={SECTION_IDS.privacyPolicyPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-8 md:mb-12">
<Button
variant="outline"
size="sm"
onClick={handleBackClick}
leftIcon={<ArrowUturnLeftIcon />}
className="mb-6"
>
{PRIVACY_POLICY_CONTENT.backButtonText}
</Button>
<h1 className="font-quicksand text-3xl sm:text-4xl md:text-5xl font-bold text-slate-900">
{PRIVACY_POLICY_CONTENT.title}
</h1>
<p className="mt-3 text-sm text-slate-500 font-inter">
{PRIVACY_POLICY_CONTENT.lastUpdated}
</p>
</header>
<article className="prose prose-lg max-w-none font-inter text-slate-700">
{PRIVACY_POLICY_CONTENT.sections.map((section, index) => (
<div key={index} className="mb-6">
{section.heading && <h2 className="font-quicksand text-2xl font-semibold mt-0 mb-3 text-slate-700">{section.heading}</h2>}
{typeof section.content === 'string' ? <p>{section.content}</p> : section.content}
</div>
))}
</article>
</div>
</section>
);
};
export default PrivacyPolicySection;

150
components/ResearchAllSection.tsx Executable file
View File

@@ -0,0 +1,150 @@
import React, { useState, useMemo } from 'react';
import { CurrentView, ResearchPaper } from '../types';
import { SECTION_IDS, RESEARCH_ALL_CONTENT } from '../constants';
import Button from './Button';
import { ArrowUturnLeftIcon } from './icons';
import ResearchCard from './ResearchCard';
import FilterSortBar, { ViewMode, SortOptionKey, SortOption } from './FilterSortBar';
interface ResearchAllSectionProps {
researchPapers: ResearchPaper[];
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const sortOptions: SortOption[] = [
{ key: 'newest', label: 'Сначала новые' },
{ key: 'oldest', label: 'Сначала старые' },
{ key: 'alphabetical', label: 'По алфавиту (А-Я)' },
];
const ResearchAllSection: React.FC<ResearchAllSectionProps> = ({ researchPapers, setCurrentView, setSelectedItemId }) => {
const [viewMode, setViewMode] = useState<ViewMode>('grid');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedSort, setSelectedSort] = useState<SortOptionKey>('newest');
const handleBackClick = () => {
setCurrentView('main');
setSelectedItemId(null);
setTimeout(() => {
document.getElementById(SECTION_IDS.latestResearch)?.scrollIntoView({ behavior: 'smooth' });
}, 0);
};
const availableCategories = useMemo(() => {
if (!researchPapers) return [];
return Array.from(new Set(researchPapers.map(p => p.category))).sort();
}, [researchPapers]);
const handleCategoryChange = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const filteredAndSortedPapers = useMemo(() => {
if (!researchPapers) return [];
const filtered = selectedCategories.length > 0
? researchPapers.filter(p => selectedCategories.includes(p.category))
: researchPapers;
return [...filtered].sort((a, b) => {
switch (selectedSort) {
case 'newest':
return new Date(b.publishedAt || 0).getTime() - new Date(a.publishedAt || 0).getTime();
case 'oldest':
return new Date(a.publishedAt || 0).getTime() - new Date(b.publishedAt || 0).getTime();
case 'alphabetical':
return a.title.localeCompare(b.title);
default:
return 0;
}
});
}, [researchPapers, selectedCategories, selectedSort]);
if (!researchPapers) {
return (
<section
id={SECTION_IDS.researchAllPage}
className="py-16 md:py-24 bg-white min-h-screen flex justify-center items-center"
>
<p className="text-slate-600 font-inter">Загрузка исследований...</p>
</section>
);
}
const layoutClasses = viewMode === 'grid'
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
: "flex flex-col gap-4";
return (
<section
id={SECTION_IDS.researchAllPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-8 md:mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900 mb-4 sm:mb-0">
{RESEARCH_ALL_CONTENT.title}
</h1>
<Button
variant="outline"
size="md"
onClick={handleBackClick}
leftIcon={<ArrowUturnLeftIcon />}
className="self-start sm:self-center"
>
{RESEARCH_ALL_CONTENT.backButtonTextPrefix} {RESEARCH_ALL_CONTENT.backButtonTextSuffix}
</Button>
</div>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-3xl">
{RESEARCH_ALL_CONTENT.subtitle}
</p>
</header>
<FilterSortBar
viewMode={viewMode}
onViewChange={setViewMode}
availableCategories={availableCategories}
selectedCategories={selectedCategories}
onCategoryChange={handleCategoryChange}
onClearCategories={() => setSelectedCategories([])}
sortOptions={sortOptions}
selectedSort={selectedSort}
onSortChange={setSelectedSort}
/>
{filteredAndSortedPapers.length > 0 ? (
<div className={layoutClasses}>
{filteredAndSortedPapers.map(paper => (
<ResearchCard
key={paper.id}
paper={paper}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
viewMode={viewMode}
/>
))}
</div>
) : (
<div className="text-center py-10">
<h2 className="text-2xl font-quicksand font-semibold text-slate-700 mb-3">{RESEARCH_ALL_CONTENT.emptyStateTitle}</h2>
<p className="text-slate-600 font-inter">
{RESEARCH_ALL_CONTENT.emptyStateMessage}
</p>
</div>
)}
</div>
</section>
);
};
export default ResearchAllSection;

98
components/ResearchCard.tsx Executable file
View File

@@ -0,0 +1,98 @@
import React from 'react';
import { ResearchPaper, CurrentView } from '../types';
import { ViewMode } from './FilterSortBar';
interface ResearchCardProps {
paper: ResearchPaper;
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
viewMode?: ViewMode;
}
const ResearchCard: React.FC<ResearchCardProps> = ({ paper, setCurrentView, setSelectedItemId, viewMode = 'grid' }) => {
const handleCardClick = () => {
setCurrentView('researchPaperDetail');
setSelectedItemId(paper.id);
};
if (viewMode === 'list') {
return (
<div
onClick={handleCardClick}
className="group block p-4 rounded-lg bg-white transition-all duration-300 ease-in-out hover:bg-slate-50 cursor-pointer w-full"
role="article"
aria-labelledby={`research-title-${paper.id}`}
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0 w-24 h-24 md:w-28 md:h-28 rounded-md overflow-hidden bg-slate-200">
<img
src={paper.imageUrl}
alt={`Визуализация для исследования: ${paper.title}`}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 ease-in-out"
loading="lazy"
/>
</div>
<div className="flex-1">
<h3 id={`research-title-${paper.id}`} className="text-base md:text-lg font-quicksand font-semibold text-slate-800 group-hover:text-black mb-1 transition-colors duration-200 leading-tight">
{paper.title}
</h3>
<p className="text-xs text-slate-500 mb-2">
<span className="font-semibold text-slate-600">{paper.category}</span>
<span className="mx-1.5 text-slate-400"></span>
<span>{paper.date}</span>
</p>
{paper.authors && (
<p className="text-xs text-slate-600 mt-2 font-inter line-clamp-1">
Авторы: {paper.authors.join(', ')}
</p>
)}
{paper.abstract && !paper.authors && (
<p className="text-sm text-slate-600 line-clamp-2 font-inter">{paper.abstract}</p>
)}
</div>
</div>
</div>
);
}
// Grid view
return (
<div
onClick={handleCardClick}
className="group block rounded-xl overflow-hidden transition-all duration-300 ease-in-out bg-white transform hover:-translate-y-1 cursor-pointer shadow-sm hover:shadow-md"
role="article"
aria-labelledby={`research-title-${paper.id}`}
>
<div className="relative w-full overflow-hidden aspect-[4/3]">
<img
src={paper.imageUrl}
alt={`Визуализация для исследования: ${paper.title}`}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 ease-in-out"
loading="lazy"
/>
</div>
<div className="p-4 md:p-5">
<h3 id={`research-title-${paper.id}`} className="font-quicksand font-semibold text-lg md:text-xl text-slate-800 group-hover:text-black mb-2 transition-colors duration-200 leading-tight">
{paper.title}
</h3>
<p className="text-xs text-slate-500">
<span className="font-semibold text-slate-600">{paper.category}</span>
<span className="mx-1.5 text-slate-400"></span>
<span>{paper.date}</span>
</p>
{paper.authors && (
<p className="text-xs text-slate-600 mt-2 font-inter line-clamp-1">
Авторы: {paper.authors.join(', ')}
</p>
)}
{paper.abstract && !paper.authors && (
<p className="text-sm text-slate-600 mt-2 font-inter line-clamp-2">{paper.abstract}</p>
)}
</div>
</div>
);
};
export default ResearchCard;

View File

@@ -0,0 +1,159 @@
import React from 'react';
import { CurrentView, ResearchPaper } from '../types';
import { SECTION_IDS, RESEARCH_PAPER_DETAIL_CONTENT } from '../constants'; // Removed mockResearchPapers
import Button from './Button';
import { ArrowUturnLeftIcon, ChevronRightIcon } from './icons';
import ItemDetailNavigation from './ItemDetailNavigation';
import ResearchCard from './ResearchCard';
import GalleryComponent from './GalleryComponent'; // Import GalleryComponent
interface ResearchPaperDetailProps {
itemId: string;
allResearchPapers: ResearchPaper[];
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const ResearchPaperDetail: React.FC<ResearchPaperDetailProps> = ({ itemId, allResearchPapers, setCurrentView, setSelectedItemId }) => {
const paper = allResearchPapers.find(p => p.id === itemId);
const handleBackClick = () => {
setCurrentView('researchAll');
setSelectedItemId(null);
};
const handleNavigateItem = (newItemId: string) => {
setSelectedItemId(newItemId);
};
const handleShowAllKeepReading = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
setCurrentView('researchAll');
setSelectedItemId(null);
};
if (!paper) {
return (
<section id={SECTION_IDS.researchPaperDetailPage} className="py-16 md:py-24 min-h-screen flex items-center justify-center">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="text-2xl font-semibold text-slate-700">{RESEARCH_PAPER_DETAIL_CONTENT.notFoundTitle}</h1>
<Button onClick={handleBackClick} leftIcon={<ArrowUturnLeftIcon />} className="mt-8">
{RESEARCH_PAPER_DETAIL_CONTENT.backButtonText}
</Button>
</div>
</section>
);
}
const currentIndex = allResearchPapers.findIndex(p => p.id === itemId);
const previousPaper = currentIndex > 0 ? allResearchPapers[currentIndex - 1] : undefined;
const nextPaper = currentIndex < allResearchPapers.length - 1 ? allResearchPapers[currentIndex + 1] : undefined;
const relatedPapers = allResearchPapers
.filter(p => p.id !== itemId)
.slice(0, 3);
return (
<section
id={SECTION_IDS.researchPaperDetailPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<Button
variant="outline"
size="sm"
onClick={handleBackClick}
leftIcon={<ArrowUturnLeftIcon />}
className="mb-6"
>
{RESEARCH_PAPER_DETAIL_CONTENT.backButtonText}
</Button>
{paper.imageUrl && (
<div className="mb-8 rounded-lg overflow-hidden">
<img src={paper.imageUrl} alt={paper.title} className="w-full h-auto object-cover max-h-[60vh]" />
</div>
)}
<div className="max-w-3xl mx-auto">
<header className="mb-8 md:mb-12">
<h1 className="font-quicksand text-3xl sm:text-4xl md:text-5xl font-bold text-slate-800 mb-3">
{paper.title}
</h1>
<div className="font-inter text-sm text-slate-500">
<span className="font-semibold text-slate-600">{paper.category}</span>
<span className="mx-1.5"></span>
<span>{paper.date}</span>
{paper.authors && paper.authors.length > 0 && (
<>
<span className="mx-1.5"></span>
<span>Авторы: {paper.authors.join(', ')}</span>
</>
)}
</div>
</header>
<article className="prose prose-lg max-w-none font-inter text-slate-700">
{paper.abstract && (
<div className="p-4 bg-slate-50 rounded-md mb-6">
<h2 className="text-xl font-semibold font-quicksand text-slate-700">{RESEARCH_PAPER_DETAIL_CONTENT.abstractTitle}</h2>
<p>{paper.abstract}</p>
</div>
)}
{typeof paper.fullContent === 'string' ? (
<div dangerouslySetInnerHTML={{ __html: paper.fullContent }} />
) : (
paper.fullContent
)}
</article>
{paper.gallery && paper.gallery.length > 0 && (
<GalleryComponent items={paper.gallery} title={RESEARCH_PAPER_DETAIL_CONTENT.galleryTitle} />
)}
<ItemDetailNavigation
previousItem={previousPaper ? { id: previousPaper.id, title: previousPaper.title } : undefined}
nextItem={nextPaper ? { id: nextPaper.id, title: nextPaper.title } : undefined}
onNavigate={handleNavigateItem}
previousItemButtonLabel={RESEARCH_PAPER_DETAIL_CONTENT.previousItemButtonLabel || `Предыдущее ${RESEARCH_PAPER_DETAIL_CONTENT.itemTypeSingular}`}
nextItemButtonLabel={RESEARCH_PAPER_DETAIL_CONTENT.nextItemButtonLabel || `Следующее ${RESEARCH_PAPER_DETAIL_CONTENT.itemTypeSingular}`}
noPreviousItemText={RESEARCH_PAPER_DETAIL_CONTENT.noPreviousItemText || `Это первое ${RESEARCH_PAPER_DETAIL_CONTENT.itemTypeSingular}`}
noNextItemText={RESEARCH_PAPER_DETAIL_CONTENT.noNextItemText || `Это последнее ${RESEARCH_PAPER_DETAIL_CONTENT.itemTypeSingular}`}
themeColorClass="slate"
/>
</div>
{relatedPapers.length > 0 && (
<div className="mt-12 md:mt-16">
<div className="flex justify-between items-baseline mb-8">
<h2 className="font-quicksand text-3xl font-bold text-slate-800">
{RESEARCH_PAPER_DETAIL_CONTENT.keepReadingTitle || "Дальнейшие исследования"}
</h2>
<a
href={`#${SECTION_IDS.researchAllPage}`}
onClick={handleShowAllKeepReading}
className="group inline-flex items-center text-base font-medium text-slate-700 hover:text-black transition-colors whitespace-nowrap"
aria-label={`Посмотреть все ${RESEARCH_PAPER_DETAIL_CONTENT.itemTypePlural}`}
>
{RESEARCH_PAPER_DETAIL_CONTENT.viewAllButtonText || `Все ${RESEARCH_PAPER_DETAIL_CONTENT.itemTypePlural}`}
<ChevronRightIcon className="ml-1 w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" />
</a>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{relatedPapers.map(relatedPaper => (
<ResearchCard
key={relatedPaper.id}
paper={relatedPaper}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
/>
))}
</div>
</div>
)}
</div>
</section>
);
};
export default ResearchPaperDetail;

65
components/ResearchSection.tsx Executable file
View File

@@ -0,0 +1,65 @@
import React from 'react';
import { CurrentView, ResearchPaper } from '../types'; // Added ResearchPaper type
import { SECTION_IDS, RESEARCH_SECTION_CONTENT } from '../constants';
import { ChevronRightIcon } from './icons';
import ResearchCard from './ResearchCard';
interface ResearchSectionProps {
researchPapers: ResearchPaper[]; // Changed from mockResearchPapers
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const ResearchSection: React.FC<ResearchSectionProps> = ({ researchPapers, setCurrentView, setSelectedItemId }) => {
const handleShowAllClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
setCurrentView('researchAll');
setSelectedItemId(null);
};
if (!researchPapers || researchPapers.length === 0) {
return (
<section id={SECTION_IDS.latestResearch} className="py-16 md:py-24 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<p className="text-slate-600 font-inter">Загрузка исследований...</p>
</div>
</section>
);
}
return (
<section id={SECTION_IDS.latestResearch} className="py-16 md:py-24 bg-white">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-baseline mb-12 md:mb-16">
<h2 className="font-quicksand text-4xl sm:text-5xl font-bold text-slate-900">
{RESEARCH_SECTION_CONTENT.title}
</h2>
<a
href={`#${SECTION_IDS.researchAllPage}`}
onClick={handleShowAllClick}
className="group inline-flex items-center text-base font-medium text-slate-700 hover:text-black transition-colors whitespace-nowrap"
aria-label={RESEARCH_SECTION_CONTENT.showAllAriaLabel}
>
{RESEARCH_SECTION_CONTENT.showAllText}
<ChevronRightIcon className="ml-1 w-5 h-5 transform group-hover:translate-x-1 transition-transform duration-200" />
</a>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8 xl:gap-10">
{researchPapers.slice(-4).reverse().map(paper => (
<ResearchCard
key={paper.id}
paper={paper}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
/>
))}
</div>
</div>
</section>
);
};
export default ResearchSection;

83
components/SafetyDiagram.tsx Executable file
View File

@@ -0,0 +1,83 @@
import React from 'react';
import { ArrowLeftIcon } from './icons';
const DiagramNode: React.FC<{ label: string; className?: string; children?: React.ReactNode }> = ({ label, className = '', children }) => (
<div className={`absolute ${className}`}>
<div className="relative w-[220px] h-[220px]">
<div className="absolute left-1/2 top-1/2 flex h-[150px] w-[150px] -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-slate-100 text-center text-xl font-semibold text-slate-700">
<span>{label}</span>
</div>
{children}
</div>
</div>
);
const OrbitingLabel: React.FC<{ label: string; className?: string }> = ({ label, className = '' }) => (
<div className={`absolute bg-slate-50 uppercase px-4 py-2 rounded-lg font-mono text-xs text-slate-600 text-center whitespace-nowrap ${className}`}>
{label}
</div>
);
const AnimatedArrowCircle: React.FC<{ duration?: string, direction?: 'normal' | 'reverse' }> = ({ duration = '30s', direction = 'normal' }) => (
<div
className="absolute inset-[-5px] border-2 border-dashed border-green-500 rounded-full"
style={{
borderColor: '#22c55e transparent transparent transparent',
animation: `spin ${duration} linear infinite`,
animationDirection: direction,
}}
>
<div className={`absolute top-0 left-1/2 -translate-x-1/2 -translate-y-full ${direction === 'reverse' ? '-rotate-90' : 'rotate-90'}`}>
<div className="h-0 w-0" style={{
borderStyle: 'solid',
borderWidth: '0 4px 7px 4px',
borderColor: 'transparent transparent #22c55e transparent',
}}></div>
</div>
</div>
);
const ConnectingArrow: React.FC<{ className?: string, rotation?: string }> = ({ className = '', rotation = '0deg' }) => (
<div className={`absolute flex items-center flex-col gap-y-1 w-10 text-slate-400 ${className}`} style={{ transform: `rotate(${rotation})` }}>
<div className="animate-safety-arrow"><ArrowLeftIcon className="w-4 h-4 rotate-180" /></div>
<div className="animate-safety-arrow" style={{animationDelay: '1s'}}><ArrowLeftIcon className="w-4 h-4 rotate-180" /></div>
</div>
);
const SafetyDiagram: React.FC = () => {
return (
<div className="relative w-full max-w-5xl mx-auto my-12" style={{ aspectRatio: '1920 / 1080' }}>
<div className="relative w-full h-full font-mono uppercase scale-[0.5] sm:scale-[0.6] md:scale-[0.75] lg:scale-[0.9] xl:scale-100 origin-top-left">
<DiagramNode label="Teach" className="left-[440px] top-[76px]">
<AnimatedArrowCircle duration="30s" direction="normal" />
<OrbitingLabel label="Filter Data" className="left-1/2 top-[-20px] -translate-x-1/2 -translate-y-full" />
<OrbitingLabel label="Human Values" className="left-[-20px] top-1/2 -translate-x-full -translate-y-1/2" />
<OrbitingLabel label="Company Policies" className="left-1/2 bottom-[-20px] -translate-x-1/2 translate-y-full" />
</DiagramNode>
<DiagramNode label="Test" className="left-[1030px] top-[76px]">
<AnimatedArrowCircle duration="40s" direction="reverse" />
<OrbitingLabel label="Red Teaming" className="left-1/2 top-[-20px] -translate-x-1/2 -translate-y-full" />
<OrbitingLabel label="Preparedness Evals" className="right-[-20px] top-1/2 translate-x-full -translate-y-1/2" />
<OrbitingLabel label="System Cards" className="left-1/2 bottom-[-20px] -translate-x-1/2 translate-y-full" />
</DiagramNode>
<DiagramNode label="Share" className="left-[735px] top-[552px]">
<AnimatedArrowCircle duration="35s" direction="normal" />
<OrbitingLabel label="Safety Committees" className="left-1/2 top-[-20px] -translate-x-1/2 -translate-y-full" />
<OrbitingLabel label="Feedback" className="left-[-20px] top-1/2 -translate-x-full -translate-y-1/2" />
<OrbitingLabel label="Alpha / Beta" className="right-[-20px] top-1/2 translate-x-full -translate-y-1/2" />
</DiagramNode>
{/* Connecting Arrows */}
<ConnectingArrow className="left-[940px] top-[282px]" />
<ConnectingArrow className="left-[1090px] top-[525px]" rotation="-55deg" />
<ConnectingArrow className="left-[790px] top-[525px] rotate-[55deg]" />
</div>
</div>
);
};
export default SafetyDiagram;

78
components/ServiceOfferCard.tsx Executable file
View File

@@ -0,0 +1,78 @@
import React from 'react';
import Button from './Button'; // Import the Button component
// Define a basic interface for a service offer, exported for potential use elsewhere
export interface ServiceOffer {
id: string;
title: string;
description: string;
price?: string;
imageUrl?: string;
imageAlt?: string;
actionText?: string;
onAction?: (id: string) => void;
themeColor?: 'purple' | 'blue' | 'teal' | 'amber'; // Optional theme color
}
interface ServiceOfferCardProps {
offer: ServiceOffer;
}
const ServiceOfferCard: React.FC<ServiceOfferCardProps> = ({ offer }) => {
const handleActionClick = () => {
if (offer.onAction) {
offer.onAction(offer.id);
}
};
return (
<div
className="bg-white rounded-xl transition-all duration-300 ease-in-out overflow-hidden flex flex-col h-full group"
role="article"
aria-labelledby={`offer-title-${offer.id}`}
>
{offer.imageUrl && (
<div className="w-full h-48 sm:h-56 bg-slate-200 overflow-hidden">
<img
src={offer.imageUrl}
alt={offer.imageAlt || `Visual for ${offer.title}`}
className="w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-300"
loading="lazy"
/>
</div>
)}
<div className="p-5 md:p-6 flex flex-col flex-grow">
<h3
id={`offer-title-${offer.id}`}
className={`font-quicksand text-xl sm:text-2xl font-bold text-slate-900 mb-2 group-hover:text-black transition-colors`}
>
{offer.title}
</h3>
<p className="font-inter text-slate-600 text-sm leading-relaxed mb-4 flex-grow">
{offer.description}
</p>
<div className="flex flex-wrap items-center justify-between mt-auto">
{offer.price && (
<p className={`font-quicksand text-xl font-bold text-slate-700 mb-3 sm:mb-0`}>
{offer.price}
</p>
)}
{offer.actionText && offer.onAction && (
<Button
variant="outline"
size="md"
onClick={handleActionClick}
className={`${!offer.price ? 'ml-auto' : ''}`}
aria-label={`${offer.actionText} for ${offer.title}`}
>
{offer.actionText}
</Button>
)}
</div>
</div>
</div>
);
};
export default ServiceOfferCard;

146
components/Sidebar.tsx Executable file
View File

@@ -0,0 +1,146 @@
import React from 'react';
import { NAV_LINKS, ABOUT_CONTEXT_NAV_LINKS, BUSINESS_CONTEXT_NAV_LINKS, EDUCATION_CONTEXT_NAV_LINKS, ACCELERATOR_CONTEXT_NAV_LINKS, SECTION_IDS, APP_NAME, FOOTER_CONTENT } from '../constants';
import { NavLinkItem, CurrentView } from '../types';
import { ChevronRightIcon } from './icons';
import Logo from './Logo'; // Import the new Logo component
interface SidebarProps {
isOpen: boolean;
onClose: () => void;
currentView: CurrentView;
setCurrentView: (view: CurrentView) => void;
isDesktopCollapsed: boolean;
}
const Sidebar: React.FC<SidebarProps> = ({ isOpen, onClose, currentView, setCurrentView, isDesktopCollapsed }) => {
const scrollToSection = (hash: string) => {
if (hash.startsWith('#')) {
const sectionId = hash.substring(1);
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
console.warn(`Element with ID ${sectionId} not found for scrolling.`);
}
} else {
console.warn(`Invalid hash for scrolling: ${hash}`);
}
};
const handleNavLinkClick = (item: NavLinkItem) => {
if (item.targetView) {
setCurrentView(item.targetView);
setTimeout(() => scrollToSection(item.href), 50); // Increased delay
} else {
setCurrentView('main');
setTimeout(() => scrollToSection(item.href), 50); // Increased delay
}
if (window.innerWidth < 768) { // md breakpoint
onClose();
}
};
const isAboutContextView = ['about', 'mission', 'careers'].includes(currentView);
const isBusinessContextView = ['businessLanding', 'businessServices'].includes(currentView);
const isEducationContextView = ['educationBusiness', 'educationStudents'].includes(currentView);
const isAcceleratorContextView = ['acceleratorAbout', 'acceleratorProjects', 'acceleratorInvestment'].includes(currentView);
let linksToRender: NavLinkItem[];
if (isAboutContextView) {
linksToRender = ABOUT_CONTEXT_NAV_LINKS;
} else if (isBusinessContextView) {
linksToRender = BUSINESS_CONTEXT_NAV_LINKS;
} else if (isEducationContextView) {
linksToRender = EDUCATION_CONTEXT_NAV_LINKS;
} else if (isAcceleratorContextView) {
linksToRender = ACCELERATOR_CONTEXT_NAV_LINKS;
} else {
linksToRender = NAV_LINKS;
}
const getLinkClasses = (item: NavLinkItem) => {
let baseClasses = `relative flex items-center p-3 rounded-lg hover:bg-slate-100
transition-colors duration-150
focus:outline-none focus:ring-2 focus:ring-slate-400 focus:bg-slate-100`;
if (item.label === 'На Главную') {
baseClasses += ' text-slate-500 hover:text-slate-700';
} else {
baseClasses += ' text-slate-700 hover:text-slate-900';
}
const isActive = (isAboutContextView || isBusinessContextView || isEducationContextView || isAcceleratorContextView) && item.targetView === currentView;
if (isActive) {
baseClasses += ' bg-slate-200 text-slate-900 font-semibold';
}
return baseClasses;
};
const sidebarClasses = `h-screen w-72 flex flex-col p-6 transform transition-all duration-300 ease-in-out
fixed top-0 left-0 z-40
bg-white/95 backdrop-blur-sm
md:bg-white md:z-30
${isOpen ? 'translate-x-0' : '-translate-x-full'}
${isDesktopCollapsed ? 'md:-translate-x-full' : 'md:translate-x-0'}`;
return (
<aside
id="sidebar-drawer"
className={sidebarClasses}
aria-label="Sidebar"
>
<div className="flex items-center justify-start mb-8 pl-1">
<a
href={`#${SECTION_IDS.hero}`}
onClick={(e) => {
e.preventDefault();
setCurrentView('main');
setTimeout(() => scrollToSection(`#${SECTION_IDS.hero}`), 50);
if (window.innerWidth < 768) onClose();
}}
className="flex items-center gap-3"
>
<Logo width={32} height={32} />
<span className={`font-quicksand text-2xl font-bold text-slate-900 transition-opacity duration-200 ${isDesktopCollapsed ? 'opacity-0' : 'opacity-100'}`}>
{APP_NAME}
</span>
</a>
</div>
<nav className="flex-grow flex flex-col justify-center overflow-y-auto no-scrollbar">
<ul className="space-y-2 font-medium">
{linksToRender.map((item: NavLinkItem) => (
<li key={item.id} className="group relative">
<a
href={item.href}
onClick={(e) => {
e.preventDefault();
handleNavLinkClick(item);
}}
className={`${getLinkClasses(item)}`}
>
{item.icon && <item.icon className="w-5 h-5 shrink-0 text-slate-500 group-hover:text-slate-700 transition-colors duration-150 mr-4" />}
<span className="font-quicksand text-base whitespace-nowrap">{item.label}</span>
{item.children && (
<ChevronRightIcon className="w-4 h-4 ml-auto opacity-50 group-hover:opacity-100 transition-opacity" />
)}
</a>
</li>
))}
</ul>
</nav>
<div className="pt-4 mt-8 border-t border-slate-200">
<p className="text-xs text-center text-slate-500">
{FOOTER_CONTENT.copyrightText(new Date().getFullYear())}
</p>
</div>
</aside>
);
};
export default Sidebar;

151
components/StoriesAllSection.tsx Executable file
View File

@@ -0,0 +1,151 @@
import React, { useState, useMemo } from 'react';
import { CurrentView, Post } from '../types';
import { SECTION_IDS, STORIES_ALL_CONTENT } from '../constants';
import Button from './Button';
import { ArrowUturnLeftIcon } from './icons';
import PostCard from './PostCard';
import FilterSortBar, { ViewMode, SortOptionKey, SortOption } from './FilterSortBar';
interface StoriesAllSectionProps {
posts: Post[];
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const sortOptions: SortOption[] = [
{ key: 'newest', label: 'Сначала новые' },
{ key: 'oldest', label: 'Сначала старые' },
{ key: 'alphabetical', label: 'По алфавиту (А-Я)' },
];
const StoriesAllSection: React.FC<StoriesAllSectionProps> = ({ posts, setCurrentView, setSelectedItemId }) => {
const [viewMode, setViewMode] = useState<ViewMode>('grid');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedSort, setSelectedSort] = useState<SortOptionKey>('newest');
const handleBackClick = () => {
setCurrentView('main');
setSelectedItemId(null);
setTimeout(() => {
document.getElementById(SECTION_IDS.story)?.scrollIntoView({ behavior: 'smooth' });
}, 0);
};
const availableCategories = useMemo(() => {
if (!posts) return [];
return Array.from(new Set(posts.map(p => p.category))).sort();
}, [posts]);
const handleCategoryChange = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const filteredAndSortedPosts = useMemo(() => {
if (!posts) return [];
const filtered = selectedCategories.length > 0
? posts.filter(p => selectedCategories.includes(p.category))
: posts;
return [...filtered].sort((a, b) => {
switch (selectedSort) {
case 'newest':
return new Date(b.publishedAt || 0).getTime() - new Date(a.publishedAt || 0).getTime();
case 'oldest':
return new Date(a.publishedAt || 0).getTime() - new Date(b.publishedAt || 0).getTime();
case 'alphabetical':
return a.title.localeCompare(b.title);
default:
return 0;
}
});
}, [posts, selectedCategories, selectedSort]);
if (!posts) {
return (
<section
id={SECTION_IDS.storiesAllPage}
className="py-16 md:py-24 bg-white min-h-screen flex justify-center items-center"
>
<p className="text-slate-600 font-inter">Загрузка проектов...</p>
</section>
);
}
const layoutClasses = viewMode === 'grid'
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
: "flex flex-col gap-4";
return (
<section
id={SECTION_IDS.storiesAllPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-8 md:mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h1 className="font-quicksand text-4xl sm:text-5xl md:text-6xl font-bold text-slate-900 mb-4 sm:mb-0">
{STORIES_ALL_CONTENT.title}
</h1>
<Button
variant="outline"
size="md"
onClick={handleBackClick}
leftIcon={<ArrowUturnLeftIcon />}
className="self-start sm:self-center"
>
{STORIES_ALL_CONTENT.backButtonTextPrefix} {STORIES_ALL_CONTENT.backButtonTextSuffix}
</Button>
</div>
<p className="mt-4 text-lg md:text-xl text-slate-700 max-w-3xl">
{STORIES_ALL_CONTENT.subtitle}
</p>
</header>
<FilterSortBar
viewMode={viewMode}
onViewChange={setViewMode}
availableCategories={availableCategories}
selectedCategories={selectedCategories}
onCategoryChange={handleCategoryChange}
onClearCategories={() => setSelectedCategories([])}
sortOptions={sortOptions}
selectedSort={selectedSort}
onSortChange={setSelectedSort}
/>
{filteredAndSortedPosts.length > 0 ? (
<div className={layoutClasses}>
{filteredAndSortedPosts.map(post => (
<PostCard
key={post.id}
post={post}
isFeatured={false}
setCurrentView={setCurrentView}
setSelectedItemId={setSelectedItemId}
viewMode={viewMode}
/>
))}
</div>
) : (
<div className="text-center py-10">
<h2 className="text-2xl font-quicksand font-semibold text-slate-700 mb-3">{STORIES_ALL_CONTENT.emptyStateTitle}</h2>
<p className="text-slate-600 font-inter">
{STORIES_ALL_CONTENT.emptyStateMessage}
</p>
</div>
)}
</div>
</section>
);
};
export default StoriesAllSection;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { CurrentView } from '../types';
import { SECTION_IDS, TERMS_OF_USE_CONTENT } from '../constants';
import Button from './Button';
import { ArrowUturnLeftIcon } from './icons';
interface TermsOfUseSectionProps {
setCurrentView: (view: CurrentView) => void;
}
const TermsOfUseSection: React.FC<TermsOfUseSectionProps> = ({ setCurrentView }) => {
const handleBackClick = () => {
setCurrentView('main');
// Optional: Scroll to top or specific section if needed
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return (
<section
id={SECTION_IDS.termsOfUsePage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-8 md:mb-12">
<Button
variant="outline"
size="sm"
onClick={handleBackClick}
className="mb-6"
leftIcon={<ArrowUturnLeftIcon />}
>
{TERMS_OF_USE_CONTENT.backButtonText}
</Button>
<h1 className="font-quicksand text-3xl sm:text-4xl md:text-5xl font-bold text-slate-900">
{TERMS_OF_USE_CONTENT.title}
</h1>
<p className="mt-3 text-sm text-slate-500 font-inter">
{TERMS_OF_USE_CONTENT.lastUpdated}
</p>
</header>
<article className="prose prose-lg max-w-none font-inter text-slate-700">
{TERMS_OF_USE_CONTENT.sections.map((section, index) => (
<div key={index} className="mb-6">
{section.heading && <h2 className="font-quicksand text-2xl font-semibold mt-0 mb-3 text-slate-700">{section.heading}</h2>}
{typeof section.content === 'string' ? <p>{section.content}</p> : section.content}
</div>
))}
</article>
</div>
</section>
);
};
export default TermsOfUseSection;

166
components/VacancyDetail.tsx Executable file
View File

@@ -0,0 +1,166 @@
import React from 'react';
import { CurrentView, Vacancy } from '../types';
import { SECTION_IDS, VACANCY_DETAIL_CONTENT } from '../constants'; // mockVacancies removed
import Button from './Button';
import { ArrowUturnLeftIcon, BriefcaseIcon, MapPinIcon, ClockIcon, EmailIcon, CheckIcon } from './icons';
interface VacancyDetailProps {
itemId: string;
allVacancies: Vacancy[]; // Changed to accept allVacancies from Strapi
setCurrentView: (view: CurrentView) => void;
setSelectedItemId: (id: string | null) => void;
}
const VacancyDetail: React.FC<VacancyDetailProps> = ({ itemId, allVacancies, setCurrentView, setSelectedItemId }) => {
const vacancy = allVacancies.find(v => v.id === itemId || v.href === itemId);
const handleBackClick = () => {
setCurrentView('careers');
setSelectedItemId(null);
setTimeout(() => {
document.getElementById(SECTION_IDS.careersPage)?.scrollIntoView({ behavior: 'smooth' });
}, 0);
};
if (!vacancy) {
return (
<section
id={SECTION_IDS.vacancyDetailPage}
className="py-16 md:py-24 bg-white min-h-screen flex items-center justify-center"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
<BriefcaseIcon className="w-16 h-16 mx-auto text-slate-400 mb-4" />
<h1 className="text-2xl font-semibold font-quicksand text-slate-700 mb-4">{VACANCY_DETAIL_CONTENT.notFoundTitle}</h1>
<p className="text-slate-600 mb-8">
{VACANCY_DETAIL_CONTENT.vacancyNotFoundMessage}
</p>
<Button
onClick={handleBackClick}
leftIcon={<ArrowUturnLeftIcon />}
variant="outline"
>
{VACANCY_DETAIL_CONTENT.backButtonText}
</Button>
</div>
</section>
);
}
const applyMailto = `mailto:${VACANCY_DETAIL_CONTENT.hrEmail}?subject=${encodeURIComponent(VACANCY_DETAIL_CONTENT.mailtoSubjectPrefix + vacancy.title)}&body=${encodeURIComponent(VACANCY_DETAIL_CONTENT.mailtoBodyPrefix + vacancy.title + "\"." + " Мое резюме во вложении.")}`;
return (
<section
id={SECTION_IDS.vacancyDetailPage}
className="py-16 md:py-24 bg-white min-h-screen"
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-8 md:mb-12">
<Button
variant="outline"
size="sm"
onClick={handleBackClick}
leftIcon={<ArrowUturnLeftIcon />}
className="mb-6"
>
{VACANCY_DETAIL_CONTENT.backButtonText}
</Button>
<h1 className="font-quicksand text-3xl sm:text-4xl md:text-5xl font-bold text-slate-800 mb-3">
{vacancy.title}
</h1>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 font-inter text-sm text-slate-600">
<div className="flex items-center">
<BriefcaseIcon className="w-4 h-4 mr-1.5 text-slate-500" />
<span>{vacancy.department}</span>
</div>
<div className="flex items-center">
<MapPinIcon className="w-4 h-4 mr-1.5 text-slate-500" />
<span>{vacancy.location}</span>
</div>
<div className="flex items-center">
<ClockIcon className="w-4 h-4 mr-1.5 text-slate-500" />
<span>{vacancy.type}</span>
</div>
</div>
</header>
<div className="bg-white p-6 sm:p-8 md:p-10 rounded-xl">
<div className="prose prose-lg max-w-none font-inter text-slate-700">
{vacancy.fullDescription && (
<>
<h2 className="font-quicksand text-xl font-semibold text-slate-700">{VACANCY_DETAIL_CONTENT.descriptionTitle}</h2>
{typeof vacancy.fullDescription === 'string' && vacancy.fullDescription.includes('<') ?
<div dangerouslySetInnerHTML={{ __html: vacancy.fullDescription }} /> :
<p>{vacancy.fullDescription}</p>
}
</>
)}
{vacancy.responsibilities && vacancy.responsibilities.length > 0 && (
<>
<h2 className="font-quicksand text-xl font-semibold text-slate-700 mt-8">{VACANCY_DETAIL_CONTENT.responsibilitiesTitle}</h2>
<ul className="list-none p-0 space-y-3">
{vacancy.responsibilities.map((item, index) => (
<li key={index} className="flex items-start">
<CheckIcon className="w-5 h-5 text-slate-500 mr-3 mt-1 flex-shrink-0" />
<span>{item}</span>
</li>
))}
</ul>
</>
)}
{vacancy.qualifications && vacancy.qualifications.length > 0 && (
<>
<h2 className="font-quicksand text-xl font-semibold text-slate-700 mt-8">{VACANCY_DETAIL_CONTENT.qualificationsTitle}</h2>
<ul className="list-none p-0 space-y-3">
{vacancy.qualifications.map((item, index) => (
<li key={index} className="flex items-start">
<CheckIcon className="w-5 h-5 text-slate-500 mr-3 mt-1 flex-shrink-0" />
<span>{item}</span>
</li>
))}
</ul>
</>
)}
{vacancy.offer && vacancy.offer.length > 0 && (
<>
<h2 className="font-quicksand text-xl font-semibold text-slate-700 mt-8">{VACANCY_DETAIL_CONTENT.offerTitle}</h2>
<ul className="list-none p-0 space-y-3">
{vacancy.offer.map((item, index) => (
<li key={index} className="flex items-start">
<CheckIcon className="w-5 h-5 text-slate-500 mr-3 mt-1 flex-shrink-0" />
<span>{item}</span>
</li>
))}
</ul>
</>
)}
</div>
<div className="mt-10 pt-8 border-t border-slate-200 text-center">
<h3 className="text-xl font-quicksand font-semibold text-slate-700 mb-4">
{VACANCY_DETAIL_CONTENT.applyPromptTitle}
</h3>
<p className="text-slate-600 font-inter mb-6 max-w-lg mx-auto">
{VACANCY_DETAIL_CONTENT.applyPromptMessage}
</p>
<Button
variant="primary"
size="lg"
leftIcon={<EmailIcon />}
onClick={() => window.location.href = applyMailto}
>
{VACANCY_DETAIL_CONTENT.applyButtonText}
</Button>
</div>
</div>
</div>
</section>
);
};
export default VacancyDetail;

148
components/icons.tsx Executable file
View File

@@ -0,0 +1,148 @@
import React from 'react';
import {
ArrowDownTrayIcon as ArrowDownTray,
SparklesIcon as Sparkles,
Bars3Icon as Hamburger,
XMarkIcon as Close,
ChevronRightIcon as ChevronRight,
ChevronDownIcon as ChevronDown, // Added
Squares2X2Icon as Squares2X2, // Added
PlayIcon as Play,
PauseIcon as Pause,
AcademicCapIcon,
ShieldCheckIcon,
CpuChipIcon,
SparklesIcon as SoraIcon, // Using SparklesIcon for Sora as a placeholder
CircleStackIcon,
BuildingOffice2Icon,
BookOpenIcon,
InformationCircleIcon,
NewspaperIcon,
EnvelopeIcon as Email, // Renaming for clarity
HomeIcon as HomeOutline, // For About context nav
UserGroupIcon as UserGroup, // For About Us
LightBulbIcon as LightBulb, // For Our Mission
BriefcaseIcon as Briefcase, // For Careers
WrenchScrewdriverIcon, // For Business Services
ArrowUturnLeftIcon as ArrowUturnLeft, // Added for back buttons
RocketLaunchIcon, // For Startup Accelerator
CubeTransparentIcon as CubeTransparent, // For Accelerator Projects
CurrencyDollarIcon, // For Accelerator Investment
BuildingLibraryIcon, // Added for Education
MapPinIcon, // Added for VacancyDetail
ClockIcon, // Added for VacancyDetail
ChartBarIcon, // Added for BusinessCourse, AcceleratorFeature
UsersIcon, // Added for AcceleratorFeature
CodeBracketIcon, // Added for BusinessCourse, StudentProgram, ServiceItemData
PaintBrushIcon, // Added for ServiceItemData
MagnifyingGlassIcon, // Added for ServiceItemData
PuzzlePieceIcon, // Added for ServiceItemData
PresentationChartLineIcon, // Added for AcceleratorInvestment
TagIcon, // Added for AcceleratorProjects
LinkIcon, // Added for AcceleratorProjects
ComputerDesktopIcon, // Added for development services
CommandLineIcon, // Added for development/programs
ArrowLeftIcon as ArrowLeft, // For item navigation
ArrowRightIcon as ArrowRight, // For item navigation
PhotoIcon as Photo, // Added for Gallery
VideoCameraIcon as VideoCamera, // Added for Gallery
ArrowUpIcon as ArrowUp,
PlusIcon,
CheckIcon,
} from '@heroicons/react/24/outline';
export const ChevronDownIcon = ChevronDown;
export const Squares2X2Icon = Squares2X2;
export const ArrowDownTrayIcon = ArrowDownTray;
export const SparklesIcon = Sparkles;
export const Bars3Icon = Hamburger;
export const XMarkIcon = Close;
export const ChevronRightIcon = ChevronRight;
export const PlayIcon = Play;
export const PauseIcon = Pause;
export const EmailIcon = Email; // Export with specific name for ConnectSection
export const ArrowUturnLeftIcon = ArrowUturnLeft; // Exporting ArrowUturnLeftIcon
export const ArrowLeftIcon = ArrowLeft;
export const ArrowRightIcon = ArrowRight;
export const PhotoIcon = Photo;
export const VideoCameraIcon = VideoCamera;
export const ArrowUpIcon = ArrowUp;
export {
CubeTransparent as CubeTransparentIcon,
PlusIcon,
AcademicCapIcon,
ShieldCheckIcon,
CpuChipIcon,
SoraIcon,
CircleStackIcon,
BuildingOffice2Icon,
BookOpenIcon,
InformationCircleIcon,
NewspaperIcon,
UserGroup as UserGroupIcon,
LightBulb as LightBulbIcon,
Briefcase as BriefcaseIcon,
WrenchScrewdriverIcon,
RocketLaunchIcon,
CurrencyDollarIcon,
BuildingLibraryIcon,
MapPinIcon,
ClockIcon,
ChartBarIcon,
UsersIcon,
CodeBracketIcon,
PaintBrushIcon,
MagnifyingGlassIcon,
PuzzlePieceIcon,
PresentationChartLineIcon,
TagIcon,
LinkIcon,
ComputerDesktopIcon,
CommandLineIcon,
CheckIcon,
HomeOutline as HomeIcon,
};
export const AdjustmentsHorizontalIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" {...props}>
<path d="M9.66667 3.33333C8.93029 3.33333 8.33333 3.93029 8.33333 4.66667C8.33333 5.40305 8.93029 6 9.66667 6C10.403 6 11 5.40305 11 4.66667C11 3.93029 10.403 3.33333 9.66667 3.33333ZM7.08401 4C7.38004 2.84985 8.42411 2 9.66667 2C10.9092 2 11.9533 2.84985 12.2493 4H13.3333C13.7015 4 14 4.29848 14 4.66667C14 5.03486 13.7015 5.33333 13.3333 5.33333H12.2493C11.9533 6.48349 10.9092 7.33333 9.66667 7.33333C8.42411 7.33333 7.38004 6.48349 7.08401 5.33333H2.66667C2.29848 5.33333 2 5.03486 2 4.66667C2 4.29848 2.29848 4 2.66667 4H7.08401ZM6.33333 10C5.59695 10 5 10.597 5 11.3333C5 12.0697 5.59695 12.6667 6.33333 12.6667C7.06971 12.6667 7.66667 12.0697 7.66667 11.3333C7.66667 10.597 7.06971 10 6.33333 10ZM3.75068 10.6667C4.04671 9.51652 5.09077 8.66667 6.33333 8.66667C7.57589 8.66667 8.61996 9.51652 8.91599 10.6667H13.3333C13.7015 10.6667 14 10.9651 14 11.3333C14 11.7015 13.7015 12 13.3333 12H8.91599C8.61996 13.1502 7.57589 14 6.33333 14C5.09077 14 4.04671 13.1502 3.75068 12H2.66667C2.29848 12 2 11.7015 2 11.3333C2 10.9651 2.29848 10.6667 2.66667 10.6667H3.75068Z" />
</svg>
);
export const ListBulletIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12" fill="currentColor" {...props}>
<path fillRule="evenodd" clipRule="evenodd" d="M1 0C0.447715 0 0 0.447715 0 1C0 1.55228 0.447715 2 1 2C1.55228 2 2 1.55228 2 1C2 0.447715 1.55228 0 1 0ZM5 0C4.44772 0 4 0.447715 4 1C4 1.55228 4.44771 2 5 2H15C15.5523 2 16 1.55228 16 1C16 0.447715 15.5523 0 15 0H5ZM0 6C0 5.44772 0.447715 5 1 5C1.55228 5 2 5.44772 2 6C2 6.55228 1.55228 7 1 7C0.447715 7 0 6.55228 0 6ZM5 5C4.44772 5 4 5.44772 4 6C4 6.55228 4.44771 7 5 7H15C15.5523 7 16 6.55228 16 6C16 5.44772 15.5523 5 15 5H5ZM0 11C0 10.4477 0.447715 10 1 10C1.55228 10 2 10.4477 2 11C2 11.5523 1.55228 12 1 12C0.447715 12 0 11.5523 0 11ZM5 10C4.44772 10 4 10.4477 4 11C4 11.5523 4.44771 12 5 12H15C15.5523 12 16 11.5523 16 11C16 10.4477 15.5523 10 15 10H5Z" />
</svg>
);
export const ArrowTopRightIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg width="11" height="12" viewBox="0 0 11 12" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M1.70985 4.5H7.7804M7.7804 4.5V10.5705M7.7804 4.5L0.780396 11.5" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"></path>
</svg>
);
export const SidebarToggleIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path fillRule="evenodd" clipRule="evenodd" d="M9.35719 3H14.6428C15.7266 2.99999 16.6007 2.99998 17.3086 3.05782C18.0375 3.11737 18.6777 3.24318 19.27 3.54497C20.2108 4.02433 20.9757 4.78924 21.455 5.73005C21.7568 6.32234 21.8826 6.96253 21.9422 7.69138C22 8.39925 22 9.27339 22 10.3572V13.6428C22 14.7266 22 15.6008 21.9422 16.3086C21.8826 17.0375 21.7568 17.6777 21.455 18.27C20.9757 19.2108 20.2108 19.9757 19.27 20.455C18.6777 20.7568 18.0375 20.8826 17.3086 20.9422C16.6008 21 15.7266 21 14.6428 21H9.35717C8.27339 21 7.39925 21 6.69138 20.9422C5.96253 20.8826 5.32234 20.7568 4.73005 20.455C3.78924 19.9757 3.02433 19.2108 2.54497 18.27C2.24318 17.6777 2.11737 17.0375 2.05782 16.3086C1.99998 15.6007 1.99999 14.7266 2 13.6428V10.3572C1.99999 9.27341 1.99998 8.39926 2.05782 7.69138C2.11737 6.96253 2.24318 6.32234 2.54497 5.73005C3.02433 4.78924 3.78924 4.02433 4.73005 3.54497C5.32234 3.24318 5.96253 3.11737 6.69138 3.05782C7.39926 2.99998 8.27341 2.99999 9.35719 3ZM6.85424 5.05118C6.24907 5.10062 5.90138 5.19279 5.63803 5.32698C5.07354 5.6146 4.6146 6.07354 4.32698 6.63803C4.19279 6.90138 4.10062 7.24907 4.05118 7.85424C4.00078 8.47108 4 9.26339 4 10.4V13.6C4 14.7366 4.00078 15.5289 4.05118 16.1458C4.10062 16.7509 4.19279 17.0986 4.32698 17.362C4.6146 17.9265 5.07354 18.3854 5.63803 18.673C5.90138 18.8072 6.24907 18.8994 6.85424 18.9488C7.47108 18.9992 8.26339 19 9.4 19H14.6C15.7366 19 16.5289 18.9992 17.1458 18.9488C17.7509 18.8994 18.0986 18.8072 18.362 18.673C18.9265 18.3854 19.3854 17.9265 19.673 17.362C19.8072 17.0986 19.8994 16.7509 19.9488 16.1458C19.9992 15.5289 20 14.7366 20 13.6V10.4C20 9.26339 19.9992 8.47108 19.9488 7.85424C19.8994 7.24907 19.8072 6.90138 19.673 6.63803C19.3854 6.07354 18.9265 5.6146 18.362 5.32698C18.0986 5.19279 17.7509 5.10062 17.1458 5.05118C16.5289 5.00078 15.7366 5 14.6 5H9.4C8.26339 5 7.47108 5.00078 6.85424 5.05118ZM7 7C7.55229 7 8 7.44772 8 8V16C8 16.5523 7.55229 17 7 17C6.44772 17 6 16.5523 6 16V8C6 7.44772 5 7 5 7Z" />
<path d="M10 8C10 7.44772 10.4477 7 11 7H13C13.5523 7 14 7.44772 14 8C14 8.55228 13.5523 9 13 9H11C10.4477 9 10 8.55228 10 8ZM11 15C10.4477 15 10 15.4477 10 16C10 16.5523 10.4477 17 11 17H13C13.5523 17 14 16.5523 14 16C14 15.4477 13.5523 15 13 15H11Z" />
</svg>
);
export const TelegramIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}><path d="M9.78 18.65l.28-4.23 7.68-6.92c.34-.31-.07-.46-.52-.19L7.74 13.3 3.64 12c-.51-.16-.52-.53.11-.7L16.94 6c.4-.15.75.12.63.53l-2.22 10.21c-.13.51-.47.64-1.01.38l-4.56-3.35-2.14 2.05c-.22.21-.4.33-.66.33z"></path></svg>
);
export const WhatsAppIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}><path d="M16.75 13.96c.25.13.43.2.5.33.07.13.07.66-.03 1.39-.1.73-.55 1.34-1.14 1.78-.59.44-1.34.58-1.9.58-.55 0-1.12-.08-1.74-.25-1.8-.5-3.35-1.42-4.63-2.82-1.49-1.63-2.5-3.5-2.82-5.1-.17-.62-.25-1.23-.25-1.78 0-.58.14-1.33.58-1.9.44-.6.99-1.07 1.72-1.17.73-.1 1.26-.1 1.39-.03.13.07.2.26.33.5.13.25.28.58.31.62.03.04.06.07.06.14 0 .07-.03.13-.06.19-.03.06-2.14 2.44-2.14 2.44s-.59.58-.59 1.02c0 .44.59 1.02.59 1.02s2.14 2.44 2.14 2.44c.06.06.13.1.19.1.07 0 .14-.03.14-.06.04-.03.36-.19.61-.31zM12 2a10 10 0 100 20 10 10 0 000-20z"></path></svg>
);
export const VKIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}><path d="M13.125 10.368l1.453-.162c.594 0 .97.234 1.13.729.135.45.032 1.152-.451 2.034-.516.945-1.11 1.836-1.11 1.836s.693 1.17 1.828 1.485c.421.126 1.026.153 1.69.153h.27s.243.027.243.162c0 .243-.648.783-1.432.891-.972.108-2.025-.135-2.97-.621-.972-.513-1.62-.972-2.16-1.296-.54-.351-.89-.27-.89-.27s-.054 1.269-.432 1.62c-.378.351-1.053.405-1.215.27-.405-.297-1.242-1.323-2.457-3.267C5.176 12.333 4 10.09 4 10.09s-.027-.189.135-.297c.135-.081.405 0 .405 0h2.43s.189 0 .297.054c.108.054.135.216.135.216s.324 1.026.702 1.674c.756 1.296 1.404 1.647 1.62 1.485.351-.27.243-1.782.243-1.782s.027-.972-.27-1.35c-.297-.378-.891-.378-1.053-.351l-1.836.216c-.243 0-.405-.081-.405-.27s.054-.27.216-.324c.162-.054.513-.108.972-.108h1.269c.594 0 1.026.054 1.296.189.27.135.378.432.243.594-.135.162-.216.216-.324.216s-.297.027-.297.027z"></path></svg>
);
export const RutubeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}><path d="M12.012 1.8a10.2 10.2 0 100 20.4 10.2 10.2 0 000-20.4zm-1.63 15.637V6.56l7.863 3.92-7.863 7.017zm-4.63-7.567a.555.555 0 01.555-.554h2.723a.555.555 0 01.554.554v2.723a.555.555 0 01-.554.554H6.307a.555.555 0 01-.555-.554V9.87z"></path></svg>
);

643
constants.ts Executable file
View File

@@ -0,0 +1,643 @@
import React from 'react';
import {
NavLinkItem, Post, NewsArticle, ResearchPaper, BusinessStory, Vacancy,
BusinessCourse, StudentProgram, AcceleratorFeature, ServiceItemData, ClientLogo,
AcceleratorProject, InvestmentProject,
HeroContent, AboutUsContent, FooterContent, FooterLink, FooterSocialLink, UITexts,
ConnectSectionContent, ContactMethodUI,
BlogSectionContent, NewsSectionContent, ResearchSectionContent, BusinessSectionContent,
OurMissionContent, MissionSection, CareersPageContent, BusinessLandingContent, BusinessServicesContent,
EducationBusinessContent, EducationStudentsContent, AllItemsPageContent,
AcceleratorAboutContent, AcceleratorProjectsContent, AcceleratorInvestmentContent,
DetailPageContent, BlogPostDetailContent, NewsArticleDetailContent, ResearchPaperDetailContent,
BusinessStoryDetailContent, VacancyDetailContent, TermsOfUseContent, PrivacyPolicyContent,
CurrentView
} from './types';
import {
AcademicCapIcon, ShieldCheckIcon, CpuChipIcon, SparklesIcon as SoraIcon,
CircleStackIcon, BuildingOffice2Icon, BookOpenIcon, InformationCircleIcon,
NewspaperIcon, UserGroupIcon, LightBulbIcon, BriefcaseIcon,
WrenchScrewdriverIcon, RocketLaunchIcon, CubeTransparentIcon, CurrencyDollarIcon, BuildingLibraryIcon,
ChartBarIcon, UsersIcon, CodeBracketIcon, PaintBrushIcon, MagnifyingGlassIcon, PuzzlePieceIcon, PresentationChartLineIcon,
TelegramIcon, WhatsAppIcon, EmailIcon, VKIcon, RutubeIcon,
ArrowDownTrayIcon, SparklesIcon, Bars3Icon, XMarkIcon, ChevronRightIcon, PlayIcon, PauseIcon,
ArrowUturnLeftIcon, MapPinIcon, ClockIcon, TagIcon, LinkIcon, ComputerDesktopIcon, CommandLineIcon,
PhotoIcon, VideoCameraIcon, // Added PhotoIcon and VideoCameraIcon
SidebarToggleIcon
} from './components/icons';
export enum SECTION_IDS {
hero = 'hero',
solutions = 'solutions',
connect = 'connect',
story = 'story',
latestNews = 'latestNews',
latestResearch = 'latestResearch',
businessShowcase = 'businessShowcase',
aboutUsPage = 'about-us-page',
ourMissionPage = 'our-mission-page',
careersPage = 'careers-page',
businessLandingPage = 'business-landing-page',
businessServicesPage = 'business-services-page',
educationBusinessPage = 'education-business-page',
educationStudentsPage = 'education-students-page',
researchAllPage = 'research-all-page',
newsAllPage = 'news-all-page',
storiesAllPage = 'stories-all-page',
acceleratorAboutPage = 'accelerator-about-page',
acceleratorProjectsPage = 'accelerator-projects-page',
acceleratorInvestmentPage = 'accelerator-investment-page',
blogPostDetailPage = 'blog-post-detail-page',
newsArticleDetailPage = 'news-article-detail-page',
researchPaperDetailPage = 'research-paper-detail-page',
businessStoryDetail = 'business-story-detail-page',
vacancyDetailPage = 'vacancy-detail-page',
termsOfUsePage = 'terms-of-use-page',
privacyPolicyPage = 'privacy-policy-page',
researchLink = 'researchLink',
education = 'education',
startupAccelerator = 'startupAccelerator',
businessLink = 'businessLink',
storiesLink = 'storiesLink',
company = 'company',
newsLink = 'newsLink',
developmentLink = 'developmentLink',
}
export const NAV_LINKS: NavLinkItem[] = [
{
id: SECTION_IDS.researchLink,
label: 'Исследования',
href: `#${SECTION_IDS.researchAllPage}`,
targetView: 'researchAll'
},
{
id: SECTION_IDS.education,
label: 'Обучение',
href: `#${SECTION_IDS.educationBusinessPage}`,
targetView: 'educationBusiness'
},
{
id: SECTION_IDS.startupAccelerator,
label: 'Акселератор',
href: `#${SECTION_IDS.acceleratorAboutPage}`,
targetView: 'acceleratorAbout'
},
{
id: SECTION_IDS.businessLink,
label: 'Для Бизнеса',
href: `#${SECTION_IDS.businessLandingPage}`,
targetView: 'businessLanding'
},
{
id: SECTION_IDS.storiesLink,
label: 'Истории',
href: `#${SECTION_IDS.storiesAllPage}`,
targetView: 'storiesAll'
},
{
id: SECTION_IDS.company,
label: 'О Нас',
href: `#${SECTION_IDS.aboutUsPage}`,
targetView: 'about'
},
{
id: SECTION_IDS.newsLink,
label: 'Новости',
href: `#${SECTION_IDS.newsAllPage}`,
targetView: 'newsAll'
},
];
export const ABOUT_CONTEXT_NAV_LINKS: NavLinkItem[] = [
{ id: 'home-context-about', label: 'На Главную', href: `#${SECTION_IDS.hero}`, icon: ArrowUturnLeftIcon, targetView: 'main' },
{ id: SECTION_IDS.aboutUsPage, label: 'О Нас', href: `#${SECTION_IDS.aboutUsPage}`, targetView: 'about' },
{ id: SECTION_IDS.ourMissionPage, label: 'Наша Миссия', href: `#${SECTION_IDS.ourMissionPage}`, targetView: 'mission' },
{ id: SECTION_IDS.careersPage, label: 'Карьера', href: `#${SECTION_IDS.careersPage}`, targetView: 'careers' },
];
export const BUSINESS_CONTEXT_NAV_LINKS: NavLinkItem[] = [
{ id: 'home-context-business', label: 'На Главную', href: `#${SECTION_IDS.hero}`, icon: ArrowUturnLeftIcon, targetView: 'main' },
{ id: SECTION_IDS.businessLandingPage, label: 'Кейсы для Бизнеса', href: `#${SECTION_IDS.businessLandingPage}`, targetView: 'businessLanding' },
{ id: SECTION_IDS.businessServicesPage, label: 'Наши Услуги', href: `#${SECTION_IDS.businessServicesPage}`, targetView: 'businessServices' },
];
export const EDUCATION_CONTEXT_NAV_LINKS: NavLinkItem[] = [
{ id: 'home-context-education', label: 'На Главную', href: `#${SECTION_IDS.hero}`, icon: ArrowUturnLeftIcon, targetView: 'main' },
{ id: SECTION_IDS.educationBusinessPage, label: 'Для Бизнеса', href: `#${SECTION_IDS.educationBusinessPage}`, targetView: 'educationBusiness' },
{ id: SECTION_IDS.educationStudentsPage, label: 'Для Студентов', href: `#${SECTION_IDS.educationStudentsPage}`, targetView: 'educationStudents' },
];
export const ACCELERATOR_CONTEXT_NAV_LINKS: NavLinkItem[] = [
{ id: 'home-context-accelerator', label: 'На Главную', href: `#${SECTION_IDS.hero}`, icon: ArrowUturnLeftIcon, targetView: 'main' },
{ id: SECTION_IDS.acceleratorAboutPage, label: 'Об Акселераторе', href: `#${SECTION_IDS.acceleratorAboutPage}`, targetView: 'acceleratorAbout' },
{ id: SECTION_IDS.acceleratorProjectsPage, label: 'Проекты Акселератора', href: `#${SECTION_IDS.acceleratorProjectsPage}`, targetView: 'acceleratorProjects' },
{ id: SECTION_IDS.acceleratorInvestmentPage, label: 'Инвестиции', href: `#${SECTION_IDS.acceleratorInvestmentPage}`, targetView: 'acceleratorInvestment' },
];
export const NAVIGABLE_VIEWS: CurrentView[] = [
'main',
'about',
'mission',
'careers',
'businessLanding',
'businessServices',
'educationBusiness',
'educationStudents',
'researchAll',
'newsAll',
'storiesAll',
'acceleratorAbout',
'acceleratorProjects',
'acceleratorInvestment',
'termsOfUse',
'privacyPolicy'
];
export const APP_NAME = "iiEasy";
export const APP_SLOGAN = "Разработка Сайтов, Приложений и ИИ-Решений в Уфе";
// --- MOCK DATA FOR CONTENT TYPES NOT YET MIGRATED TO STRAPI ---
// mockVacancies removed, will be fetched from Strapi
export const mockAcceleratorFeatures: AcceleratorFeature[] = [
{
icon: LightBulbIcon,
title: 'Центр ИИ-Инноваций в Уфе',
description: 'Доступ к технологиям ИИ и ресурсам для разработки ИИ-решений, сайтов и приложений в Уфе.',
},
{
icon: UsersIcon,
title: 'Менторство от Экспертов ИИ (Уфа)',
description: 'Опытные менторы из нашего центра ИИ-исследований в Уфе помогут в разработке ИИ-программ и бизнес-моделей.',
},
{
icon: ChartBarIcon,
title: 'Инвестиции в ИИ-Стартапы Уфы',
description: 'Возможность представить свои ИИ-решения инвесторам. Поддержка ИИ-стартапов в Уфе.',
},
{
icon: RocketLaunchIcon,
title: 'Быстрый Рост ИИ-Проектов',
description: 'Наша программа акселерации ИИ-стартапов в Уфе нацелена на интенсивное развитие и масштабирование ИИ-решений.',
},
];
// --- END OF MOCK DATA TO BE KEPT (for now) ---
// --- START OF CENTRALIZED TEXT CONTENT ---
export const HERO_CONTENT: HeroContent = {
titlePart1: "Разработка Сайтов, Приложений и ИИ-Решений",
titlePart2Gradient: "",
subtitle: `iiEasy (Уфа): создаем сайты, приложения, программы. Внедряем передовые ИИ-решения. Ведущий центр ИИ-исследований и обучения. Трансформируем бизнес с помощью ИИ.`,
primaryButtonText: "Наши ИИ-Решения",
secondaryButtonText: "Связаться с нами",
scrollDownAriaLabel: "Прокрутить к нашим историям",
};
export const ABOUT_US_CONTENT: AboutUsContent = {
title: "О Нас: Центр Разработки и ИИ-Исследований в Уфе",
paragraph1: `Добро пожаловать в iiEasy! Мы — команда разработчиков, исследователей и энтузиастов из Уфы, объединенных страстью к созданию сайтов, приложений, программ и передовых ИИ-решений. Наша цель — стать ведущим центром ИИ-исследований и обучения искусственному интеллекту в Уфе.`,
paragraph2: `В iiEasy мы не просто создаем продукты, мы строим будущее. Наша экспертиза в разработке веб-сайтов, мобильных приложений и программного обеспечения сочетается с глубокими знаниями в области искусственного интеллекта. Мы активно развиваем направление ИИ-исследований и предлагаем образовательные программы по обучению ИИ для специалистов и энтузиастов в Уфе.`,
teamTitle: "Наша Команда Разработчиков и ИИ-Экспертов (Уфа)",
teamParagraph: "Наша команда в Уфе состоит из высококвалифицированных специалистов с опытом в разработке сайтов, приложений, программ, машинном обучении, NLP, CV и анализе данных. Мы гордимся культурой непрерывного обучения ИИ и стремлением быть на переднем крае технологий.",
valuesTitle: "Наши Ценности",
valuesList: [
{ strong: "Инновации в Разработке:", text: "Мы постоянно ищем новые подходы для создания лучших сайтов, приложений и ИИ-решений." },
{ strong: "Ответственный ИИ:", text: "Мы осознаем важность этики ИИ и стремимся к созданию безопасных ИИ-программ." },
{ strong: "Качество Продуктов:", text: "Мы уделяем внимание деталям при разработке каждого сайта, приложения или ИИ-решения." },
{ strong: "Сотрудничество и Партнерство:", text: "Мы верим, что лучшие ИИ-решения создаются в команде." },
{ strong: "Экспертиза и Обучение ИИ:", text: "Мы не только создаем ИИ-решения, но и активно делимся знаниями, стремясь стать центром ИИ-компетенций и обучения ИИ в Уфе." }
],
closingParagraph: "Присоединяйтесь к iiEasy (Уфа) на пути к созданию интеллектуального будущего с помощью передовой разработки и ИИ-решений!",
};
const COMMON_CONTACT_METHODS_DATA = {
telegram: {
id: 'telegram',
name: 'Telegram',
href: 'https://t.me/iiEasy',
icon: TelegramIcon,
bgColor: 'bg-slate-100',
hoverBgColor: 'hover:bg-slate-200',
ringColor: 'focus:ring-slate-300',
},
whatsapp: {
id: 'whatsapp',
name: 'WhatsApp',
href: 'https://wa.me/79638908700',
icon: WhatsAppIcon,
bgColor: 'bg-slate-100',
hoverBgColor: 'hover:bg-slate-200',
ringColor: 'focus:ring-slate-300',
},
email: {
id: 'email',
name: 'Email',
href: 'mailto:hello@iieasy.ru',
icon: EmailIcon,
bgColor: 'bg-slate-100',
hoverBgColor: 'hover:bg-slate-200',
ringColor: 'focus:ring-slate-300',
}
};
export const FOOTER_CONTENT: FooterContent = {
legalTitle: "Правовая информация",
legalLinks: [
{ text: "Правила использования", href: `#${SECTION_IDS.termsOfUsePage}`, targetView: 'termsOfUse' },
{ text: "Политика конфиденциальности", href: `#${SECTION_IDS.privacyPolicyPage}`, targetView: 'privacyPolicy' },
],
copyrightText: (year) => `© ${year} ${APP_NAME}.`,
tagline: APP_SLOGAN,
credits: "Создано с ❤️, ИИ и экспертизой в разработке.",
socialTitle: "Мы в соц сетях",
socialLinks: [
{ name: "Telegram", href: "https://t.me/iiEasy", icon: TelegramIcon, ariaLabel: "Telegram iiEasy Уфа" },
{ name: "VK", href: "https://vk.com/iieasy", icon: VKIcon, ariaLabel: "VK iiEasy Уфа" },
{ name: "Rutube", href: "https://rutube.ru/channel/66664248/", icon: RutubeIcon, ariaLabel: "Rutube iiEasy Уфа" },
],
address: "г. Уфа, ул. Комсомольская, 19/1 (Центр ИИ-Исследований и Разработки)",
email: "hello@iieasy.ru",
emailHref: "mailto:hello@iieasy.ru",
companyName: 'ООО "ИЗИ ГРУПП"',
companyInn: '0277953363',
};
export const CONNECT_SECTION_CONTENT: ConnectSectionContent = {
title: "Свяжитесь с Нами в Уфе",
subtitle: "Обсудим ваш проект по разработке сайта, приложения или ИИ-решения? Хотите узнать больше о наших ИИ-исследованиях или программах обучения ИИ в Уфе? Свяжитесь с нами!",
contactMethods: [
{ ...COMMON_CONTACT_METHODS_DATA.telegram, ariaLabel: "Связаться с iiEasy (Уфа) в Telegram" },
{ ...COMMON_CONTACT_METHODS_DATA.whatsapp, ariaLabel: "Связаться с iiEasy (Уфа) в WhatsApp" },
{ ...COMMON_CONTACT_METHODS_DATA.email, ariaLabel: "Написать iiEasy (Уфа) на Email", href: "mailto:hello@iieasy.ru" },
],
};
export const BLOG_SECTION_CONTENT: BlogSectionContent = {
title: "Истории",
titleGradientPart: "",
showAllText: "Все истории",
showAllAriaLabel: "Посмотреть все наши истории и идеи о разработке и ИИ",
};
export const NEWS_SECTION_CONTENT: NewsSectionContent = {
title: "Новости",
titleGradientPart: "",
showAllText: "Все новости",
showAllAriaLabel: "Посмотреть все новости о разработке и ИИ в Уфе",
};
export const RESEARCH_SECTION_CONTENT: ResearchSectionContent = {
title: "Исследования",
titleGradientPart: "",
showAllText: "Все исследования",
showAllAriaLabel: "Посмотреть все ИИ-исследования нашего центра в Уфе",
};
export const BUSINESS_SECTION_CONTENT: BusinessSectionContent = {
title: "Кейсы для Бизнеса",
titleGradientPart: "",
showAllText: "Все кейсы",
showAllAriaLabel: "Посмотреть все кейсы внедрения ИИ-решений и разработки",
};
export const OUR_MISSION_CONTENT: OurMissionContent = {
title: "Манифест iiEasy",
preamble: React.createElement(React.Fragment, null,
React.createElement("p", null, "Мы, компания iiEasy, верим, что искусственный интеллект — это мощнейший инструмент для позитивных преобразований в экономике и обществе. Наша миссия — применять передовые технологии ИИ для обеспечения устойчивого развития Башкортостана и России, решая прикладные задачи для государства и бизнеса."),
React.createElement("p", { className: "mt-4" }, "Этот Манифест определяет фундаментальные принципы, которыми мы руководствуемся в нашей работе.")
),
sections: [
{
title: "1. Прикладная польза для региона и страны",
content: "Наш главный приоритет — создание решений, приносящих реальную, измеримую пользу. Мы концентрируем наши усилия на проектах, которые способствуют повышению качества жизни граждан, росту эффективности бизнеса и укреплению экономики нашего региона и страны. Мы не занимаемся исследованиями в вакууме; каждая наша разработка, от систем анализа данных до языковых моделей, нацелена на решение конкретных проблем. Успех для нас — это не только технологическое достижение, но и позитивные изменения, которые оно привносит в реальный мир."
},
{
title: "2. Ответственность и надежность",
content: React.createElement(React.Fragment, null,
React.createElement("p", null, "Осознавая влияние наших технологий на жизнь людей и работу организаций, мы принимаем на себя полную ответственность за их надежность, безопасность и этичность. Мы обязуемся:"),
React.createElement("ul", { className: "list-disc space-y-2 pl-5 mt-4" },
React.createElement("li", null, React.createElement("strong", null, "Обеспечивать безопасность данных:"), " Защита конфиденциальной информации и персональных данных является высшим приоритетом во всех наших системах."),
React.createElement("li", null, React.createElement("strong", null, "Стремиться к объективности:"), " Мы активно работаем над выявлением и снижением алгоритмической предвзятости, чтобы наши ИИ-системы были справедливыми и беспристрастными."),
React.createElement("li", null, React.createElement("strong", null, "Гарантировать прозрачность:"), " Мы стремимся к тому, чтобы принципы работы наших моделей были понятны партнерам, обеспечивая человеческий контроль над критически важными системами.")
)
)
},
{
title: "3. Технологический суверенитет и лидерство",
content: "Мы стремимся сделать Башкортостан одним из ведущих ИИ-регионов России, а Россию — одним из мировых лидеров в области искусственного интеллекта. Для достижения этой цели мы фокусируемся на развитии технологического суверенитета: разрабатываем собственные решения, адаптируем лучшие мировые практики и инвестируем в исследования, снижающие зависимость от зарубежных технологий. Важнейшей частью этой работы является создание и развитие языковых моделей для государственных языков, включая башкирский, что способствует сохранению культурного кода и обеспечивает цифровое равенство для всех граждан."
},
{
title: "4. Открытое партнерство и развитие экосистемы",
content: React.createElement(React.Fragment, null,
React.createElement("p", null, "Мы убеждены, что прорывные результаты рождаются в синергии. Мы открыты для сотрудничества с государственными учреждениями, коммерческими компаниями, научными и образовательными организациями. Наша цель — не только собственный рост, но и развитие всей ИИ-экосистемы. Мы вносим свой вклад через:"),
React.createElement("ul", { className: "list-disc space-y-2 pl-5 mt-4" },
React.createElement("li", null, React.createElement("strong", null, "Обмен знаниями:"), " Организацию образовательных программ, митапов и конференций."),
React.createElement("li", null, React.createElement("strong", null, "Формирование кадрового резерва:"), " Сотрудничество с вузами и создание стажировок для молодых специалистов."),
React.createElement("li", null, React.createElement("strong", null, "Поддержку сообщества:"), " Активное участие в жизни ИТ-сообщества Уфы и Башкортостана.")
)
)
}
],
closingStatement: "Этот Манифест — наш компас. Он направляет наши стратегические решения, определяет нашу внутреннюю культуру и служит обещанием для всех, с кем мы работаем. Мы трудимся не просто над кодом, а над будущим, в котором интеллект служит процветанию нашего дома."
};
export const CAREERS_PAGE_CONTENT: CareersPageContent = {
title: "Карьера в Центре Разработки и ИИ (Уфа)",
titleGradientPart: "",
subtitle: "Присоединяйтесь к команде iiEasy в Уфе! Мы ищем таланты для разработки сайтов, приложений, программ и ИИ-решений. Создавайте будущее вместе с нами!",
noVacanciesTitle: "Сейчас активных вакансий в Уфе нет",
noVacanciesMessagePt1: "В данный момент мы не ведем активный набор. Однако, наш центр разработки и ИИ-исследований в Уфе всегда ищет таланты.",
noVacanciesMessagePt2: "Мы внимательно изучим вашу кандидатуру и свяжемся, как только появится подходящая возможность в Уфе.",
noVacanciesContactEmailText: "hello@iieasy.ru (Уфа)",
hrEmail: "hello@iieasy.ru",
notFoundTitle: "Не нашли вакансию в разработке или ИИ (Уфа)?",
notFoundMessage: "Если ваши навыки в разработке сайтов, приложений, программ или ИИ могут быть полезны iiEasy (Уфа), расскажите нам о себе!",
applyButtonText: "Отправить резюме в iiEasy (Уфа)",
applySubject: "Хочу работать в iiEasy (Уфа) - Разработка/ИИ",
};
export const BUSINESS_LANDING_CONTENT: BusinessLandingContent = {
title: "Кейсы: Решения и Разработка",
titleGradientPart: "",
subtitle: "Узнайте, как наши ИИ-решения и услуги по разработке сайтов, приложений и программ помогают компаниям достигать результатов и оптимизировать процессы.",
emptyStateTitle: "Кейсы по разработке и ИИ скоро появятся",
emptyStateMessage: "Мы активно документируем наши проекты по разработке и ИИ-решениям. Следите за обновлениями от нашего центра в Уфе!",
};
const businessServicesContactMethods: ContactMethodUI[] = [
{ ...COMMON_CONTACT_METHODS_DATA.telegram, href: "https://t.me/iiEasy", ariaLabel: "Обсудить разработку сайта/приложения или ИИ-решение в Telegram (iiEasy Уфа)" },
{ ...COMMON_CONTACT_METHODS_DATA.whatsapp, href: `https://wa.me/79638908700?text=${encodeURIComponent("Здравствуйте, хочу обсудить разработку или ИИ-решение (iiEasy Уфа)")}`, ariaLabel: "Обсудить разработку или ИИ-решение в WhatsApp (iiEasy Уфа)" },
{ ...COMMON_CONTACT_METHODS_DATA.email, href: `mailto:hello@iieasy.ru?subject=${encodeURIComponent("Обсуждение разработки/ИИ-решения (iiEasy Уфа)")}`, ariaLabel: "Написать Email для обсуждения разработки или ИИ-решения (iiEasy Уфа)" },
];
export const BUSINESS_SERVICES_CONTENT: BusinessServicesContent = {
title: "Услуги: Разработка и Интеграция",
titleGradientPart: "",
subtitle: "Предлагаем комплексные услуги: разработка сайтов, приложений, программ, внедрение ИИ. Цифровая трансформация вашего бизнеса от iiEasy (Уфа).",
trustedByTitle: "Нам доверяют разработку и ИИ-решения",
ctaTitle: "Готовы обсудить разработку или ИИ-проект?",
ctaSubtitle: "Свяжитесь с нами в Уфе, чтобы узнать, как iiEasy поможет вашему бизнесу с разработкой сайтов, приложений или внедрением ИИ-решений.",
contactMethods: businessServicesContactMethods,
};
const educationBusinessContactMethods: ContactMethodUI[] = [
{ ...COMMON_CONTACT_METHODS_DATA.telegram, href: "https://t.me/iiEasy", ariaLabel: "Обсудить обучение ИИ для бизнеса в Telegram (iiEasy Уфа)" },
{ ...COMMON_CONTACT_METHODS_DATA.whatsapp, href: `https://wa.me/79638908700?text=${encodeURIComponent("Здравствуйте, хочу обсудить обучение ИИ для бизнеса (iiEasy Уфа)")}`, ariaLabel: "Обсудить обучение ИИ для бизнеса в WhatsApp (iiEasy Уфа)" },
{ ...COMMON_CONTACT_METHODS_DATA.email, href: `mailto:hello@iieasy.ru?subject=${encodeURIComponent("Обсуждение обучения ИИ для бизнеса (iiEasy Уфа)")}`, ariaLabel: "Написать Email для обсуждения обучения ИИ для бизнеса (iiEasy Уфа)" },
];
export const EDUCATION_BUSINESS_CONTENT: EducationBusinessContent = {
title: "Обучение для Бизнеса",
titleGradientPart: "",
subtitle: "Повысьте ИИ-компетенции вашей компании с нашими программами обучения в Уфе. Корпоративные тренинги и курсы по ИИ-решениям.",
ctaTitle: "Запросить корпоративное обучение ИИ (Уфа)",
ctaSubtitle: "Свяжитесь с центром обучения ИИ iiEasy в Уфе для разработки индивидуальной программы для вашей команды.",
contactMethods: educationBusinessContactMethods,
};
const educationStudentsContactMethods: ContactMethodUI[] = [
{ ...COMMON_CONTACT_METHODS_DATA.telegram, href: "https://t.me/iiEasy", ariaLabel: "Запрос информации об обучении ИИ для студентов в Telegram (iiEasy Уфа)" },
{ ...COMMON_CONTACT_METHODS_DATA.whatsapp, href: `https://wa.me/79638908700?text=${encodeURIComponent("Здравствуйте, хочу информацию об обучении ИИ для студентов (iiEasy Уфа)")}`, ariaLabel: "Запрос информации об обучении ИИ для студентов в WhatsApp (iiEasy Уфа)" },
{ ...COMMON_CONTACT_METHODS_DATA.email, href: `mailto:hello@iieasy.ru?subject=${encodeURIComponent("Запрос информации об обучении ИИ для студентов (iiEasy Уфа)")}`, ariaLabel: "Написать Email для запроса информации об обучении ИИ для студентов (iiEasy Уфа)" },
];
export const EDUCATION_STUDENTS_CONTENT: EducationStudentsContent = {
title: "Обучение для Студентов и Школьников",
titleGradientPart: "",
subtitle: "Откройте мир ИИ в Уфе! Курсы и программы от iiEasy для молодых талантов, стремящихся создавать ИИ-решения и программы.",
ctaTitle: "Узнать больше об обучении ИИ в Уфе",
ctaSubtitle: "Заинтересовались программами обучения ИИ от iiEasy в Уфе? Свяжитесь с нами для получения подробной информации.",
contactMethods: educationStudentsContactMethods,
};
export const STORIES_ALL_CONTENT: AllItemsPageContent = {
title: "Все Истории",
titleGradientPart: "",
backButtonTextPrefix: "К разделу",
backButtonTextSuffix: "историй",
subtitle: "Истории, статьи и инсайты о разработке сайтов, приложений, программ и ИИ-решений от команды iiEasy (Уфа).",
emptyStateTitle: "Историй пока нет",
emptyStateMessage: "Мы готовим интересный контент о нашей разработке и ИИ-исследованиях в Уфе. Следите за обновлениями!",
};
export const NEWS_ALL_CONTENT: AllItemsPageContent = {
title: "Все Новости",
titleGradientPart: "",
backButtonTextPrefix: "К разделу",
backButtonTextSuffix: "новостей",
subtitle: "Последние события в мире ИИ, разработки сайтов и приложений. Достижения нашего центра ИИ-исследований в Уфе.",
emptyStateTitle: "Новостей о разработке и ИИ пока нет",
emptyStateMessage: "Мы скоро добавим сюда свежие материалы из Уфы. Заглядывайте позже!",
};
export const RESEARCH_ALL_CONTENT: AllItemsPageContent = {
title: "Все Исследования",
titleGradientPart: "",
backButtonTextPrefix: "К разделу",
backButtonTextSuffix: "ИИ-исследований",
subtitle: "Полный список публикаций, статей и докладов от центра ИИ-исследований iiEasy в Уфе. Наша работа по созданию ИИ-решений.",
emptyStateTitle: "ИИ-исследования скоро появятся",
emptyStateMessage: "Наш центр ИИ-исследований в Уфе активно работает над обновлением этого раздела. Следите за новостями!",
};
const acceleratorContactMethods: ContactMethodUI[] = [
{ ...COMMON_CONTACT_METHODS_DATA.telegram, href: "https://t.me/iiEasy", ariaLabel: "Запрос условий участия в ИИ-Акселераторе в Telegram (iiEasy Уфа)" },
{ ...COMMON_CONTACT_METHODS_DATA.whatsapp, href: `https://wa.me/79638908700?text=${encodeURIComponent("Здравствуйте, хочу узнать условия ИИ-Акселератора (iiEasy Уфа)")}`, ariaLabel: "Запрос условий участия в ИИ-Акселераторе в WhatsApp (iiEasy Уфа)" },
{ ...COMMON_CONTACT_METHODS_DATA.email, href: `mailto:hello@iieasy.ru?subject=${encodeURIComponent("Запрос условий ИИ-Акселератора (iiEasy Уфа)")}`, ariaLabel: "Написать Email для запроса условий ИИ-Акселератора (iiEasy Уфа)" },
];
export const ACCELERATOR_ABOUT_CONTENT: AcceleratorAboutContent = {
title: "Стартап Акселератор",
titleGradientPart: "",
subtitle: "Запускаем будущее ИИ-инноваций в Уфе! Наш акселератор поддерживает ИИ-стартапы, работающие над созданием ИИ-программ и приложений.",
whatWeOfferTitle: "Что мы предлагаем стартапам в Уфе?",
whatWeOfferParagraph: "Акселератор iiEasy (Уфа) это программа для превращения идей в успешные ИИ-компании. Мы ищем команды с прорывными ИИ-решениями, способными изменить индустрии.",
ctaTitle: "Готовы подать заявку в ИИ-Акселератор (Уфа)?",
ctaSubtitle: "Если у вас есть инновационный ИИ-проект (приложение, программа, ИИ-решение) и команда, ждем вашу заявку в наш уфимский акселератор.",
contactMethods: acceleratorContactMethods,
};
export const ACCELERATOR_PROJECTS_CONTENT: AcceleratorProjectsContent = {
title: "Проекты Акселератора",
titleGradientPart: "",
subtitle: "Инновационные ИИ-стартапы из Уфы, прошедшие нашу программу и создающие ИИ-решения, сайты и приложения.",
emptyStateTitle: "Проекты ИИ-акселератора (Уфа) скоро будут здесь",
emptyStateMessage: "Мы работаем над пополнением портфолио выпускников нашего ИИ-акселератора в Уфе. Заглядывайте позже!",
projectWebsiteButtonText: "Сайт ИИ-проекта",
};
const acceleratorInvestorContactMethods: ContactMethodUI[] = [
{ ...COMMON_CONTACT_METHODS_DATA.telegram, href: "https://t.me/iiEasy", ariaLabel: "Связаться с ИИ-акселератором в Telegram (Инвесторы Уфа)" },
{ ...COMMON_CONTACT_METHODS_DATA.whatsapp, href: `https://wa.me/79638908700?text=${encodeURIComponent("Здравствуйте, хочу обсудить инвестиции в ИИ-стартапы Уфы")}`, ariaLabel: "Связаться с ИИ-акселератором в WhatsApp (Инвесторы Уфа)" },
{ ...COMMON_CONTACT_METHODS_DATA.email, href: `mailto:hello@iieasy.ru?subject=${encodeURIComponent("Инвестиции в ИИ-стартапы Уфы")}`, ariaLabel: "Написать Email ИИ-акселератору (Инвесторы Уфа)" },
];
export const ACCELERATOR_INVESTMENT_CONTENT: AcceleratorInvestmentContent = {
title: "Проекты в Поиске Инвестиций",
titleGradientPart: "",
subtitle: "Перспективные ИИ-стартапы из Уфы, готовые к росту. Эти проекты ищут партнеров для масштабирования своих ИИ-решений, программ и приложений.",
problemLabel: "Проблема:",
solutionLabel: "ИИ-Решение:",
teamLabel: "Команда (Уфа):",
fundingLabel: "Ищут:",
contactProjectButtonText: "Связаться с ИИ-проектом (Уфа)",
requestContactButtonText: "Запросить контакт (скоро)",
emptyStateTitle: "ИИ-проектов Уфы, ищущих инвестиции, пока нет",
emptyStateMessage: "Следите за обновлениями! Новые ИИ-стартапы из Уфы скоро появятся здесь.",
investorCtaTitle: "Хотите инвестировать в ИИ-стартапы Уфы?",
investorCtaSubtitle: "Если вы заинтересованы в поддержке инновационных ИИ-стартапов из Уфы и хотите узнать больше, свяжитесь с нами.",
contactMethods: acceleratorInvestorContactMethods,
};
export const BLOG_POST_DETAIL_CONTENT: BlogPostDetailContent = {
notFoundTitle: "История или статья не найдены.",
backButtonText: "Все истории",
itemTypeSingular: "история",
itemTypePlural: "истории",
galleryTitle: "Галерея Истории",
keepReadingTitle: "Читайте также",
viewAllButtonText: "Все истории",
previousItemButtonLabel: "Предыдущая история",
nextItemButtonLabel: "Следующая история",
noPreviousItemText: "Это первая история",
noNextItemText: "Это последняя история",
};
export const NEWS_ARTICLE_DETAIL_CONTENT: NewsArticleDetailContent = {
notFoundTitle: "Новость не найдена.",
backButtonText: "Все новости",
itemTypeSingular: "новость",
itemTypePlural: "новости",
galleryTitle: "Галерея Новостей",
keepReadingTitle: "Другие новости",
viewAllButtonText: "Все новости",
previousItemButtonLabel: "Предыдущая новость",
nextItemButtonLabel: "Следующая новость",
noPreviousItemText: "Это первая новость",
noNextItemText: "Это последняя новость",
};
export const RESEARCH_PAPER_DETAIL_CONTENT: ResearchPaperDetailContent = {
notFoundTitle: "ИИ-исследование не найдено.",
backButtonText: "Все ИИ-исследования (Уфа)",
abstractTitle: "Аннотация ИИ-исследования",
itemTypeSingular: "исследование",
itemTypePlural: "исследования",
galleryTitle: "Галерея Исследования",
keepReadingTitle: "Дальнейшие исследования",
viewAllButtonText: "Все исследования",
previousItemButtonLabel: "Предыдущее исследование",
nextItemButtonLabel: "Следующее исследование",
noPreviousItemText: "Это первое исследование",
noNextItemText: "Это последнее исследование",
};
export const BUSINESS_STORY_DETAIL_CONTENT: BusinessStoryDetailContent = {
notFoundTitle: "Кейс по разработке или ИИ не найден.",
backButtonText: "Все кейсы",
itemTypeSingular: "кейс",
itemTypePlural: "кейсы",
galleryTitle: "Галерея Кейса",
keepReadingTitle: "Другие кейсы",
viewAllButtonText: "Все кейсы",
previousItemButtonLabel: "Предыдущий кейс",
nextItemButtonLabel: "Следующий кейс",
noPreviousItemText: "Это первый кейс",
noNextItemText: "Это последний кейс",
};
export const VACANCY_DETAIL_CONTENT: VacancyDetailContent = {
notFoundTitle: "Вакансия не найдена.",
vacancyNotFoundMessage: "Возможно, эта вакансия в нашем центре разработки и ИИ в Уфе была закрыта или ссылка устарела.",
backButtonText: "Все вакансии (Уфа)",
descriptionTitle: "Описание вакансии (Разработка/ИИ, Уфа)",
responsibilitiesTitle: "Основные обязанности (Разработка/ИИ):",
qualificationsTitle: "Требования (Разработка/ИИ):",
offerTitle: "Мы предлагаем в Уфе:",
applyPromptTitle: "Заинтересовались вакансией в Уфе?",
applyPromptMessage: "Отправьте резюме в наш центр разработки и ИИ-исследований в Уфе, и мы свяжемся с вами.",
applyButtonText: "Откликнуться на вакансию (Уфа)",
hrEmail: "hello@iieasy.ru",
mailtoSubjectPrefix: "Отклик на вакансию (Уфа): ",
mailtoBodyPrefix: "Здравствуйте, я хотел(а) бы откликнуться на вакансию в iiEasy (Уфа) \"",
};
export const UI_TEXTS: UITexts = {
playVideoAriaLabel: "Play video",
pauseVideoAriaLabel: "Pause video",
sidebarToggleOpenAriaLabel: "Открыть навигационное меню сайта iiEasy Уфа",
sidebarToggleCloseAriaLabel: "Закрыть навигационное меню сайта iiEasy Уфа",
};
const getCurrentDate = () => new Date().toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
export const TERMS_OF_USE_CONTENT: TermsOfUseContent = {
title: "Правила использования Сервиса iiEasy",
pageTitleGradientPart: "",
lastUpdated: `Дата последнего обновления: ${getCurrentDate()}`,
backButtonText: "На главную",
sections: [
{
heading: "1. Общие положения",
content: "Настоящие Правила использования (далее «Правила») регулируют отношения между ООО \"ИЗИ ГРУПП\" (далее «Сервис») и Пользователем сети Интернет (далее «Пользователь») возникающие при использовании сайта iieasy.example.com (далее «Сайт»). Пользователь обязан полностью ознакомиться с настоящими Правилами до момента начала использования Сайта. Использование Сайта Пользователем означает полное и безоговорочное принятие Пользователем настоящих Правил в соответствии со ст. 438 Гражданского кодекса Российской Федерации."
},
{
heading: "2. Интеллектуальная собственность",
content: "Все объекты, доступные при помощи Сайта, в том числе элементы дизайна, текст, графические изображения, иллюстрации, видео, программы для ЭВМ, базы данных, музыка, звуки и другие объекты (далее «Контент Сайта»), а также любой контент, размещенный на Сайте, являются объектами исключительных прав Сервиса, Пользователей и других правообладателей. Использование Контента Сайта, а также любого контента, размещенного на Сайте, возможно только в рамках функционала, предлагаемого Сайтом. Никакие элементы содержания Сайта, а также любой контент, размещенный на Сайте, не могут быть использованы иным образом без предварительного разрешения правообладателя."
},
{
heading: "3. Ограничение ответственности",
content: "Сайт и его функционал, включая все скрипты, приложения, контент и оформление Сайта поставляются «как есть». Сервис отказывается от всяких гарантий того, что Сайт или его функционал могут подходить или не подходить для конкретных целей использования. Сервис не может гарантировать и не обещает никаких специфических результатов от использования Сайта и/или его функционала. Сервис не несет ответственности за любые прямые или косвенные убытки, произошедшие из-за использования Сайта или отдельных частей/функционалов Сайта."
},
{
heading: "4. Заключительные положения",
content: "Настоящие Правила составляют соглашение между Пользователем и Сервисом относительно порядка использования Сайта и его функционала и заменяют собой все предыдущие соглашения между Пользователем и Сервисом. Настоящие Правила регулируются и толкуются в соответствии с законодательством Российской Федерации. Вопросы, не урегулированные Правилами, подлежат разрешению в соответствии с законодательством Российской Федерации. В случае возникновения любых споров или разногласий, связанных с исполнением настоящих Правил, Пользователь и Сервис приложат все усилия для их разрешения путем проведения переговоров между ними."
}
]
};
export const PRIVACY_POLICY_CONTENT: PrivacyPolicyContent = {
title: "Политика конфиденциальности iiEasy",
pageTitleGradientPart: "",
lastUpdated: `Дата последнего обновления: ${getCurrentDate()}`,
backButtonText: "На главную",
sections: [
{
heading: "1. Введение",
content: "Настоящая Политика конфиденциальности персональных данных (далее «Политика») действует в отношении всей информации, которую ООО \"ИЗИ ГРУПП\" (далее «Компания») может получить о Пользователе во время использования сайта iieasy.example.com (далее «Сайт»). Использование Сайта означает безоговорочное согласие Пользователя с настоящей Политикой и указанными в ней условиями обработки его персональной информации; в случае несогласия с этими условиями Пользователь должен воздержаться от использования Сайта."
},
{
heading: "2. Какие данные мы собираем",
content: "При посещении Сайта мы можем собирать следующую информацию: IP-адрес, тип браузера, время доступа, адрес запрашиваемой страницы. Эта информация используется исключительно для статистического анализа и для предотвращения и решения технических проблем. Мы не собираем целенаправленно персональные данные, позволяющие идентифицировать личность, если только вы добровольно не предоставляете их нам через контактные формы или электронную почту."
},
{
heading: "3. Как мы используем ваши данные",
content: "Информация, собираемая автоматически (IP-адрес и т.д.), используется для улучшения работы Сайта и анализа его использования. Информация, предоставленная добровольно (например, через форму обратной связи), используется для ответа на ваши запросы или предоставления запрошенных услуг."
},
{
heading: "4. Файлы cookie",
content: "Сайт может использовать файлы cookie для улучшения пользовательского опыта. Файлы cookie это небольшие текстовые файлы, которые сохраняются на вашем устройстве. Вы можете настроить свой браузер таким образом, чтобы он блокировал или предупреждал вас об этих файлах, но некоторые части сайта могут перестать работать корректно."
},
{
heading: "5. Безопасность данных",
content: "Мы принимаем разумные технические и организационные меры для защиты вашей информации от несанкционированного доступа, изменения, раскрытия или уничтожения."
},
{
heading: "6. Изменения в Политике конфиденциальности",
content: "Компания оставляет за собой право вносить изменения в настоящую Политику. При внесении изменений в актуальной редакции указывается дата последнего обновления. Новая редакция Политики вступает в силу с момента ее размещения на Сайте, если иное не предусмотрено новой редакцией Политики."
},
{
heading: "7. Контактная информация",
content: `Если у вас есть вопросы относительно настоящей Политики конфиденциальности, пожалуйста, свяжитесь с нами по адресу электронной почты: ${FOOTER_CONTENT.email}.`
}
]
};
// --- END OF CENTRALIZED TEXT CONTENT ---

65
deploy-nomad.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Deploy iiEasy to Nomad
# Usage:
# export NOMAD_ADDR=http://192.168.1.16:4646
# export NOMAD_HOST=192.168.1.16
# export NOMAD_USER=its
# ./deploy-nomad.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
NOMAD_HOST="${NOMAD_HOST:-192.168.1.16}"
NOMAD_USER="${NOMAD_USER:-its}"
NOMAD_ADDR="${NOMAD_ADDR:-http://${NOMAD_HOST}:4646}"
STRAPI_PUBLIC_URL="${STRAPI_PUBLIC_URL:-http://${NOMAD_HOST}:1337}"
echo "=== iiEasy Nomad Deploy ==="
echo "Nomad: $NOMAD_ADDR"
echo "Strapi URL (for frontend): $STRAPI_PUBLIC_URL"
echo ""
# 1. Build Docker images
echo "Building frontend..."
docker build --build-arg VITE_STRAPI_URL="$STRAPI_PUBLIC_URL" -t iieasy-frontend:latest .
echo "Building Strapi..."
docker build -t iieasy-strapi:latest ./iiEasy
# 2. Save images and copy to Nomad node
echo "Saving images..."
docker save iieasy-frontend:latest iieasy-strapi:latest -o /tmp/iieasy-images.tar
echo "Copying to $NOMAD_USER@$NOMAD_HOST..."
scp /tmp/iieasy-images.tar "$NOMAD_USER@$NOMAD_HOST:/tmp/"
# 3. On remote: load images, create dirs, run job
echo "Loading images and deploying on Nomad node..."
ssh "$NOMAD_USER@$NOMAD_HOST" bash -s << REMOTE
set -e
docker load -i /tmp/iieasy-images.tar
rm -f /tmp/iieasy-images.tar
# Create host volume dirs (if using client-host-volumes.hcl.example)
sudo mkdir -p /opt/nomad/iieasy/postgres-data /opt/nomad/iieasy/uploads
sudo chown -R 999:999 /opt/nomad/iieasy/postgres-data 2>/dev/null || true
sudo chmod -R 755 /opt/nomad/iieasy/postgres-data /opt/nomad/iieasy/uploads
echo "Images loaded. Run 'nomad job run' from a machine with Nomad CLI."
REMOTE
# 4. Run Nomad job (from local machine if nomad CLI and NOMAD_ADDR are set)
if command -v nomad &>/dev/null; then
echo ""
echo "Submitting Nomad job..."
export NOMAD_ADDR
nomad job run nomad/iieasy.nomad.hcl
echo ""
echo "Done. Check: nomad job status iieasy"
else
echo ""
echo "Nomad CLI not found. Copy nomad/ to the server and run:"
echo " NOMAD_ADDR=http://127.0.0.1:4646 nomad job run iieasy.nomad.hcl"
fi

38
deploy-on-node.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Run this ON the Nomad node after sync-to-nomad.sh
# Builds images locally and runs Nomad job
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
NOMAD_HOST="${NOMAD_HOST:-192.168.1.16}"
STRAPI_PUBLIC_URL="${STRAPI_PUBLIC_URL:-http://${NOMAD_HOST}:1337}"
echo "=== iiEasy Build & Deploy (on node) ==="
echo "Strapi URL: $STRAPI_PUBLIC_URL"
echo ""
# 1. Build Docker images
echo "Building frontend..."
docker build --build-arg VITE_STRAPI_URL="$STRAPI_PUBLIC_URL" -t iieasy-frontend:latest .
echo "Building Strapi..."
docker build -t iieasy-strapi:latest ./iiEasy
# 2. Create host volume dirs
echo "Creating host volume dirs..."
sudo mkdir -p /opt/nomad/iieasy/postgres-data /opt/nomad/iieasy/uploads
sudo chmod -R 755 /opt/nomad/iieasy/postgres-data /opt/nomad/iieasy/uploads
# 3. Run Nomad job
echo ""
echo "Submitting Nomad job..."
export NOMAD_ADDR="${NOMAD_ADDR:-http://127.0.0.1:4646}"
nomad job run nomad/iieasy.nomad.hcl
echo ""
echo "Done. Check: nomad job status iieasy"
echo "Frontend: http://${NOMAD_HOST}:3000"
echo "Strapi: http://${NOMAD_HOST}:1337/admin"

36
docker-compose.yml Normal file
View File

@@ -0,0 +1,36 @@
version: '3.8'
services:
# БЭКЕНД: Strapi (находится в подпапке iiEasy)
strapi:
container_name: iieasy_backend
build:
context: ./iiEasy
dockerfile: Dockerfile
restart: always
ports:
- "1340:1340"
# Пробрасываем папку с загрузками, чтобы картинки не удалились при обновлении контейнера
volumes:
- ./iiEasy/public/uploads:/opt/app/public/uploads
- ./iiEasy/.tmp:/opt/app/.tmp
env_file:
- ./iiEasy/.env
environment:
NODE_ENV: production
# ФРОНТЕНД: Сайт с Tailwind (находится в текущей папке)
frontend:
container_name: iieasy_frontend
build:
context: .
dockerfile: Dockerfile
restart: always
ports:
- "85:80" # Твой запрос: заходим через 85 порт
depends_on:
- strapi
networks:
default:
name: iieasy_network

14
iiEasy/.dockerignore Normal file
View File

@@ -0,0 +1,14 @@
node_modules
.git
.gitignore
.env
.env.*
*.log
dist
build
.cache
.tmp
.idea
.vscode
*.md
!README.md

8
iiEasy/.env.example Executable file
View File

@@ -0,0 +1,8 @@
HOST=0.0.0.0
PORT=1337
APP_KEYS="toBeModified1,toBeModified2"
API_TOKEN_SALT=tobemodified
ADMIN_JWT_SECRET=tobemodified
TRANSFER_TOKEN_SALT=tobemodified
JWT_SECRET=tobemodified
ENCRYPTION_KEY=tobemodified

131
iiEasy/.gitignore vendored Executable file
View File

@@ -0,0 +1,131 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
.tmp
*.log
*.sql
*.sqlite
*.sqlite3
############################
# Misc.
############################
*#
ssl
.idea
nbproject
public/uploads/*
!public/uploads/.gitkeep
.tsbuildinfo
.eslintcache
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
node_modules
.node_history
############################
# Package managers
############################
.yarn/*
!.yarn/cache
!.yarn/unplugged
!.yarn/patches
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.pnp.*
yarn-error.log
############################
# Tests
############################
coverage
############################
# Strapi
############################
.env
license.txt
exports
.strapi
dist
build
.strapi-updater.json
.strapi-cloud.json

37
iiEasy/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
FROM node:20-alpine
# Устанавливаем ВСЕ нужные системные зависимости (для sharp, sqlite и сборки)
RUN apk update && apk add --no-cache \
build-base \
gcc \
autoconf \
automake \
zlib-dev \
libpng-dev \
nasm \
bash \
vips-dev \
python3 \
make \
g++
WORKDIR /opt/app
# Копируем файлы зависимостей
COPY package.json package-lock.json* yarn.lock* ./
# Устанавливаем зависимости (используем ci для стабильности)
RUN npm ci --omit=dev
# Копируем весь код проекта
COPY . .
# Собираем админку Strapi
ENV NODE_ENV=production
RUN npm run build
# Открываем порт
EXPOSE 1340
# Запуск
CMD ["npm", "run", "start"]

61
iiEasy/README.md Executable file
View File

@@ -0,0 +1,61 @@
# 🚀 Getting started with Strapi
Strapi comes with a full featured [Command Line Interface](https://docs.strapi.io/dev-docs/cli) (CLI) which lets you scaffold and manage your project in seconds.
### `develop`
Start your Strapi application with autoReload enabled. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-develop)
```
npm run develop
# or
yarn develop
```
### `start`
Start your Strapi application with autoReload disabled. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-start)
```
npm run start
# or
yarn start
```
### `build`
Build your admin panel. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-build)
```
npm run build
# or
yarn build
```
## ⚙️ Deployment
Strapi gives you many possible deployment options for your project including [Strapi Cloud](https://cloud.strapi.io). Browse the [deployment section of the documentation](https://docs.strapi.io/dev-docs/deployment) to find the best solution for your use case.
```
yarn strapi deploy
```
## 📚 Learn more
- [Resource center](https://strapi.io/resource-center) - Strapi resource center.
- [Strapi documentation](https://docs.strapi.io) - Official Strapi documentation.
- [Strapi tutorials](https://strapi.io/tutorials) - List of tutorials made by the core team and the community.
- [Strapi blog](https://strapi.io/blog) - Official Strapi blog containing articles made by the Strapi team and the community.
- [Changelog](https://strapi.io/changelog) - Find out about the Strapi product updates, new features and general improvements.
Feel free to check out the [Strapi GitHub repository](https://github.com/strapi/strapi). Your feedback and contributions are welcome!
## ✨ Community
- [Discord](https://discord.strapi.io) - Come chat with the Strapi community including the core team.
- [Forum](https://forum.strapi.io/) - Place to discuss, ask questions and find answers, show your Strapi project and get feedback or just talk with other Community members.
- [Awesome Strapi](https://github.com/strapi/awesome-strapi) - A curated list of awesome things related to Strapi.
---
<sub>🤫 Psst! [Strapi is hiring](https://strapi.io/careers).</sub>

20
iiEasy/config/admin.ts Executable file
View File

@@ -0,0 +1,20 @@
export default ({ env }) => ({
auth: {
secret: env('ADMIN_JWT_SECRET'),
},
apiToken: {
salt: env('API_TOKEN_SALT'),
},
transfer: {
token: {
salt: env('TRANSFER_TOKEN_SALT'),
},
},
secrets: {
encryptionKey: env('ENCRYPTION_KEY'),
},
flags: {
nps: env.bool('FLAG_NPS', true),
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
},
});

7
iiEasy/config/api.ts Executable file
View File

@@ -0,0 +1,7 @@
export default {
rest: {
defaultLimit: 25,
maxLimit: 100,
withCount: true,
},
};

60
iiEasy/config/database.ts Executable file
View File

@@ -0,0 +1,60 @@
import path from 'path';
export default ({ env }) => {
const client = env('DATABASE_CLIENT', 'sqlite');
const connections = {
mysql: {
connection: {
host: env('DATABASE_HOST', 'localhost'),
port: env.int('DATABASE_PORT', 3306),
database: env('DATABASE_NAME', 'strapi'),
user: env('DATABASE_USERNAME', 'strapi'),
password: env('DATABASE_PASSWORD', 'strapi'),
ssl: env.bool('DATABASE_SSL', false) && {
key: env('DATABASE_SSL_KEY', undefined),
cert: env('DATABASE_SSL_CERT', undefined),
ca: env('DATABASE_SSL_CA', undefined),
capath: env('DATABASE_SSL_CAPATH', undefined),
cipher: env('DATABASE_SSL_CIPHER', undefined),
rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true),
},
},
pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
},
postgres: {
connection: {
connectionString: env('DATABASE_URL'),
host: env('DATABASE_HOST', 'localhost'),
port: env.int('DATABASE_PORT', 5432),
database: env('DATABASE_NAME', 'strapi'),
user: env('DATABASE_USERNAME', 'strapi'),
password: env('DATABASE_PASSWORD', 'strapi'),
ssl: env.bool('DATABASE_SSL', false) && {
key: env('DATABASE_SSL_KEY', undefined),
cert: env('DATABASE_SSL_CERT', undefined),
ca: env('DATABASE_SSL_CA', undefined),
capath: env('DATABASE_SSL_CAPATH', undefined),
cipher: env('DATABASE_SSL_CIPHER', undefined),
rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true),
},
schema: env('DATABASE_SCHEMA', 'public'),
},
pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
},
sqlite: {
connection: {
filename: path.join(__dirname, '..', '..', env('DATABASE_FILENAME', '.tmp/data.db')),
},
useNullAsDefault: true,
},
};
return {
connection: {
client,
...connections[client],
acquireConnectionTimeout: env.int('DATABASE_CONNECTION_TIMEOUT', 60000),
},
};
};

12
iiEasy/config/middlewares.ts Executable file
View File

@@ -0,0 +1,12 @@
export default [
'strapi::logger',
'strapi::errors',
'strapi::security',
'strapi::cors',
'strapi::poweredBy',
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
];

1
iiEasy/config/plugins.ts Executable file
View File

@@ -0,0 +1 @@
export default () => ({});

7
iiEasy/config/server.ts Executable file
View File

@@ -0,0 +1,7 @@
export default ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1340),
app: {
keys: env.array('APP_KEYS'),
},
});

View File

24
iiEasy/docker-compose.yml Normal file
View File

@@ -0,0 +1,24 @@
version: '3.8'
services:
# Бэкенд Strapi
strapi:
build:
context: ./iiEasy
dockerfile: Dockerfile
restart: always
ports:
- "1340:1340"
volumes:
- ./iiEasy/public/uploads:/opt/app/public/uploads
env_file: ./iiEasy/.env
# Фронтенд (Сайт + Tailwind)
frontend:
build:
context: .
dockerfile: Dockerfile
restart: always
ports:
- "85:80" # Внешний порт 85, внутренний в контейнере Nginx — 80
depends_on:
- strapi

BIN
iiEasy/favicon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

20197
iiEasy/package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

44
iiEasy/package.json Executable file
View File

@@ -0,0 +1,44 @@
{
"name": "ii-easy",
"version": "0.1.0",
"private": true,
"description": "A Strapi application",
"scripts": {
"build": "strapi build",
"typecheck": "tsc --noEmit",
"console": "strapi console",
"deploy": "strapi deploy",
"dev": "strapi develop",
"develop": "strapi develop",
"start": "strapi start",
"strapi": "strapi",
"upgrade": "npx @strapi/upgrade latest",
"upgrade:dry": "npx @strapi/upgrade latest --dry"
},
"dependencies": {
"@strapi/plugin-cloud": "^5.9.0",
"@strapi/plugin-users-permissions": "^5.34.0",
"@strapi/strapi": "^5.34.0",
"better-sqlite3": "11.3.0",
"pg": "^8.11.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-router-dom": "^6.0.0",
"sharp": "0.32.6",
"styled-components": "^6.0.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "^5"
},
"engines": {
"node": ">=18.0.0 <=22.x.x",
"npm": ">=6.0.0"
},
"strapi": {
"uuid": "8f456ae0-6ae8-4025-af42-1d0290d958af",
"installId": "81fe8cf6218075e184577eb74c455ff530f9914a3c821fe5eceec184f6c35aae"
}
}

3
iiEasy/public/robots.txt Executable file
View File

@@ -0,0 +1,3 @@
# To prevent search engines from seeing the site altogether, uncomment the next two lines:
# User-Agent: *
# Disallow: /

0
iiEasy/public/uploads/.gitkeep Executable file
View File

View File

@@ -0,0 +1,37 @@
import type { StrapiApp } from '@strapi/strapi/admin';
export default {
config: {
locales: [
// 'ar',
// 'fr',
// 'cs',
// 'de',
// 'dk',
// 'es',
// 'he',
// 'id',
// 'it',
// 'ja',
// 'ko',
// 'ms',
// 'nl',
// 'no',
// 'pl',
// 'pt-BR',
// 'pt',
// 'ru',
// 'sk',
// 'sv',
// 'th',
// 'tr',
// 'uk',
// 'vi',
// 'zh-Hans',
// 'zh',
],
},
bootstrap(app: StrapiApp) {
console.log(app);
},
};

20
iiEasy/src/admin/tsconfig.json Executable file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["../plugins/**/admin/src/**/*", "./"],
"exclude": ["node_modules/", "build/", "dist/", "**/*.test.ts"]
}

View File

@@ -0,0 +1,12 @@
import { mergeConfig, type UserConfig } from 'vite';
export default (config: UserConfig) => {
// Important: always return the modified config
return mergeConfig(config, {
resolve: {
alias: {
'@': '/src',
},
},
});
};

0
iiEasy/src/api/.gitkeep Executable file
View File

View File

@@ -0,0 +1,48 @@
{
"kind": "collectionType",
"collectionName": "accelerator_projects",
"info": {
"singularName": "accelerator-project",
"pluralName": "accelerator-projects",
"displayName": "accelerator-project"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string",
"required": true
},
"tagline": {
"type": "string"
},
"description": {
"type": "text"
},
"image": {
"type": "media",
"multiple": false,
"allowedTypes": [
"images"
]
},
"category": {
"type": "string"
},
"websiteUrl": {
"type": "string"
},
"statuspro": {
"type": "string"
},
"order": {
"type": "integer"
},
"slug": {
"type": "uid",
"targetField": "name"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* accelerator-project controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::accelerator-project.accelerator-project');

View File

@@ -0,0 +1,7 @@
/**
* accelerator-project router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::accelerator-project.accelerator-project');

View File

@@ -0,0 +1,7 @@
/**
* accelerator-project service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::accelerator-project.accelerator-project');

View File

@@ -0,0 +1,32 @@
{
"kind": "collectionType",
"collectionName": "business_courses",
"info": {
"singularName": "business-course",
"pluralName": "business-courses",
"displayName": "business-course"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"title": {
"type": "string",
"required": true
},
"description": {
"type": "text"
},
"icon": {
"type": "string"
},
"order": {
"type": "integer"
},
"slug": {
"type": "uid",
"targetField": "title"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* business-course controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::business-course.business-course');

View File

@@ -0,0 +1,7 @@
/**
* business-course router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::business-course.business-course');

View File

@@ -0,0 +1,7 @@
/**
* business-course service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::business-course.business-course');

View File

@@ -0,0 +1,39 @@
{
"kind": "collectionType",
"collectionName": "business_stories",
"info": {
"singularName": "business-storie",
"pluralName": "business-stories",
"displayName": "business-storie"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"title": {
"type": "string",
"required": true
},
"category": {
"type": "string"
},
"image": {
"type": "media",
"multiple": false,
"allowedTypes": [
"images"
]
},
"description": {
"type": "text"
},
"slug": {
"type": "uid",
"targetField": "title"
},
"fullContent": {
"type": "richtext"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* business-storie controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::business-storie.business-storie');

View File

@@ -0,0 +1,7 @@
/**
* business-storie router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::business-storie.business-storie');

View File

@@ -0,0 +1,7 @@
/**
* business-storie service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::business-storie.business-storie');

View File

@@ -0,0 +1,32 @@
{
"kind": "collectionType",
"collectionName": "client_logos",
"info": {
"singularName": "client-logo",
"pluralName": "client-logos",
"displayName": "client-logo"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string",
"required": true
},
"image": {
"type": "media",
"multiple": false,
"allowedTypes": [
"images"
]
},
"websiteUrl": {
"type": "string"
},
"order": {
"type": "integer"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* client-logo controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::client-logo.client-logo');

View File

@@ -0,0 +1,7 @@
/**
* client-logo router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::client-logo.client-logo');

View File

@@ -0,0 +1,7 @@
/**
* client-logo service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::client-logo.client-logo');

View File

@@ -0,0 +1,51 @@
{
"kind": "collectionType",
"collectionName": "investment_opportunities",
"info": {
"singularName": "investment-opportunitie",
"pluralName": "investment-opportunities",
"displayName": "investment-project"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string",
"required": true
},
"industry": {
"type": "string"
},
"problem": {
"type": "blocks"
},
"solution": {
"type": "blocks"
},
"teamSize": {
"type": "integer"
},
"fundingSought": {
"type": "text"
},
"image": {
"type": "media",
"multiple": false,
"allowedTypes": [
"images"
]
},
"contactEmail": {
"type": "email"
},
"order": {
"type": "integer"
},
"slug": {
"type": "uid",
"targetField": "name"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* investment-opportunitie controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::investment-opportunitie.investment-opportunitie');

View File

@@ -0,0 +1,7 @@
/**
* investment-opportunitie router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::investment-opportunitie.investment-opportunitie');

View File

@@ -0,0 +1,7 @@
/**
* investment-opportunitie service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::investment-opportunitie.investment-opportunitie');

View File

@@ -0,0 +1,43 @@
{
"kind": "collectionType",
"collectionName": "news_articles",
"info": {
"singularName": "news-article",
"pluralName": "news-articles",
"displayName": "news-article"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"title": {
"type": "string",
"required": true
},
"category": {
"type": "string"
},
"date": {
"type": "date",
"required": true
},
"image": {
"type": "media",
"multiple": false,
"allowedTypes": [
"images"
]
},
"description": {
"type": "text"
},
"fullContent": {
"type": "blocks"
},
"slug": {
"type": "uid",
"targetField": "title"
}
}
}

View File

@@ -0,0 +1,7 @@
/**
* news-article controller
*/
import { factories } from '@strapi/strapi'
export default factories.createCoreController('api::news-article.news-article');

View File

@@ -0,0 +1,7 @@
/**
* news-article router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::news-article.news-article');

Some files were not shown because too many files have changed in this diff Show More