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

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;