Files
iiEasy/App.tsx

497 lines
24 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;