Initial commit for iiEasy: all files included
This commit is contained in:
497
App.tsx
Executable file
497
App.tsx
Executable 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;
|
||||
Reference in New Issue
Block a user