refactor: change routing to astro

This commit is contained in:
Zotov Ivan Yuryevich
2026-04-04 13:56:22 +03:00
parent 05d6e7f6d5
commit ad3655c285
40 changed files with 4923 additions and 820 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
dist
dist-ssr
.astro
*.local
# Editor

11
astro.config.mjs Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'astro/config'
import react from '@astrojs/react'
import tailwindcss from '@tailwindcss/vite'
// https://astro.build/config
export default defineConfig({
integrations: [react()],
vite: {
plugins: [tailwindcss()],
},
})

View File

@@ -1,17 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="SOFTCLICK - российская IT-компания, специализирующаяся на импортозамещении и внедрении отечественных решений"
/>
<title>SOFTCLICK - Российские IT-решения с 2021 года</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5030
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,12 @@
"npm": ">=9.0.0"
},
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/react": "^4.4.2",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2",
@@ -39,6 +40,7 @@
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"astro": "^5.16.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -62,10 +64,8 @@
"@types/node": "^20.11.19",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.7.0",
"prettier": "^3.8.1",
"tailwindcss": "^4.1.12",
"typescript": "~5.6.3",
"vite": "^6.3.5"
"typescript": "~5.6.3"
}
}

6
plan.md Normal file
View File

@@ -0,0 +1,6 @@
- [x] Починить подскролл, убрать ворнинг про useLayoutEffect из консоли
- [ ] Вынести services и vendors куда-то в статику, а не код
- [ ] Сделать фейковое апи чтобы страницы ходили за нужным им контентом, который я вынесу выше
- [ ] Собрать все views в одну страницу, которая ходила бы в апи и получало нужный контент оттуда
- [ ] Сделать динамические блоки (декомпозировать)

View File

@@ -1,130 +0,0 @@
import { useState, useEffect } from 'react'
import { Header } from './components/Header'
import { Footer } from './components/Footer'
import { ContactModal } from './components/ContactModal'
import { HomePage } from './pages/HomePage'
import { CloudPage } from './pages/CloudPage'
import { RussianSoftwarePage } from './pages/RussianSoftwarePage'
import { CybersecurityPage } from './pages/CybersecurityPage'
import { AISolutionsPage } from './pages/AISolutionsPage'
import { RussianHardwarePage } from './pages/RussianHardwarePage'
import { HardwareSolutionsPage } from './pages/HardwareSolutionsPage'
import { CADandGISPage } from './pages/CADandGISPage'
import { IndustrialSolutionsPage } from './pages/IndustrialSolutionsPage'
import { GovernmentPage } from './pages/GovernmentPage'
import { BusinessPage } from './pages/BusinessPage'
import { ConsultingPage } from './pages/ConsultingPage'
import { ImplementationPage } from './pages/ImplementationPage'
import { ManagedServicesPage } from './pages/ManagedServicesPage'
import { IntegrationPage } from './pages/IntegrationPage'
import { SpecializedServicesPage } from './pages/SpecializedServicesPage'
import { SupportPage } from './pages/SupportPage'
import { SubscriptionPage } from './pages/SubscriptionPage'
export default function App() {
const [currentPage, setCurrentPage] = useState('home')
const [pendingScroll, setPendingScroll] = useState<string | null>(null)
const [isContactModalOpen, setIsContactModalOpen] = useState(false)
const scrollToSection = (sectionId: string) => {
// Даем странице время отрендериться
setTimeout(() => {
const section = document.getElementById(sectionId)
console.log('Trying to scroll to:', sectionId, 'Found element:', section)
if (section) {
const headerHeight = 85 // Header высота + минимальный отступ
const targetPosition = section.offsetTop - headerHeight
console.log('Scrolling to position:', targetPosition)
window.scrollTo({
top: targetPosition,
behavior: 'smooth',
})
} else {
console.error('Section not found:', sectionId)
}
}, 100)
}
const handleNavigate = (page: string, section?: string) => {
if (section) {
// Клик на Кейсы/О компании/Контакты
if (currentPage !== 'home') {
// Если не на главной - переходим туда
setCurrentPage('home')
setPendingScroll(section)
} else {
// Уже на главной - скроллим сразу
scrollToSection(section)
}
} else {
// Обычная навигация на другие страницы
setCurrentPage(page)
setPendingScroll(null)
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}
useEffect(() => {
// Когда вернулись на главную с отложенным скроллом
if (currentPage === 'home' && pendingScroll) {
scrollToSection(pendingScroll)
setPendingScroll(null)
}
}, [currentPage, pendingScroll])
const handleBackToHome = () => {
setCurrentPage('home')
setPendingScroll(null)
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const renderPage = () => {
switch (currentPage) {
case 'cloud':
return <CloudPage onBack={handleBackToHome} />
case 'russian-software':
return <RussianSoftwarePage onBack={handleBackToHome} />
case 'cybersecurity':
return <CybersecurityPage onBack={handleBackToHome} />
case 'ai-solutions':
return <AISolutionsPage onBack={handleBackToHome} />
case 'russian-hardware':
return <RussianHardwarePage onBack={handleBackToHome} />
case 'hardware-solutions':
return <HardwareSolutionsPage onBack={handleBackToHome} />
case 'cad-gis':
return <CADandGISPage onBack={handleBackToHome} />
case 'industrial-solutions':
return <IndustrialSolutionsPage onBack={handleBackToHome} />
case 'government':
return <GovernmentPage onBack={handleBackToHome} />
case 'business':
return <BusinessPage onBack={handleBackToHome} />
case 'consulting':
return <ConsultingPage onBack={handleBackToHome} />
case 'implementation':
return <ImplementationPage onBack={handleBackToHome} />
case 'managed-services':
return <ManagedServicesPage onBack={handleBackToHome} />
case 'integration':
return <IntegrationPage onBack={handleBackToHome} />
case 'specialized-services':
return <SpecializedServicesPage onBack={handleBackToHome} />
case 'support':
return <SupportPage onBack={handleBackToHome} />
case 'subscription':
return <SubscriptionPage onBack={handleBackToHome} />
default:
return <HomePage onOpenContactModal={() => setIsContactModalOpen(true)} />
}
}
return (
<div className="min-h-screen bg-white w-full overflow-x-hidden">
<Header onNavigate={handleNavigate} />
{renderPage()}
<Footer />
<ContactModal isOpen={isContactModalOpen} onClose={() => setIsContactModalOpen(false)} />
</div>
)
}

View File

@@ -11,22 +11,21 @@ interface CategoryPageProps {
title: string
description: string
vendors: Vendor[]
onBack: () => void
}
export function CategoryPage({ title, description, vendors, onBack }: CategoryPageProps) {
export function CategoryPage({ title, description, vendors }: CategoryPageProps) {
return (
<div className="min-h-screen bg-gray-50">
{/* Hero секция */}
<div className="bg-white border-b border-gray-100 pt-32 pb-16">
<div className="max-w-[1600px] mx-auto px-6 xl:px-12 2xl:px-16">
<button
onClick={onBack}
className="flex items-center gap-2 text-gray-600 hover:text-blue-600 mb-8 transition-colors group"
<a
href="/"
className="inline-flex items-center gap-2 text-gray-600 hover:text-blue-600 mb-8 transition-colors group"
>
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
Назад к главной
</button>
</a>
<div className="flex items-start gap-8">
<div className="flex-1">

View File

@@ -1,12 +1,9 @@
import { Menu, X, ChevronDown } from 'lucide-react'
import { useState } from 'react'
import { ContactModal } from './ContactModal'
import { navigateHomeSectionAnchor } from '../lib/scrollToHash'
interface HeaderProps {
onNavigate?: (page: string, section?: string) => void
}
export function Header({ onNavigate }: HeaderProps) {
export function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [isSolutionsOpen, setIsSolutionsOpen] = useState(false)
const [isServicesOpen, setIsServicesOpen] = useState(false)
@@ -35,26 +32,12 @@ export function Header({ onNavigate }: HeaderProps) {
{ name: 'Услуги по подписке', path: 'subscription' },
]
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, sectionId: string) => {
e.preventDefault()
console.log('Header: handleNavClick called with sectionId:', sectionId)
onNavigate?.('home', sectionId)
setIsMenuOpen(false)
}
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-100">
<nav className="max-w-[1600px] mx-auto px-6 xl:px-12 2xl:px-16 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-12">
<a
href="#"
onClick={e => {
e.preventDefault()
onNavigate?.('home')
}}
className="flex items-center gap-3 group"
>
<a href="/" className="flex items-center gap-3 group">
<span className="text-2xl text-gray-900 tracking-wide font-bold">SOFTCLICK</span>
</a>
@@ -73,16 +56,13 @@ export function Header({ onNavigate }: HeaderProps) {
<div className="absolute top-full left-0 pt-2">
<div className="w-80 bg-white rounded-xl shadow-xl border border-gray-100 py-3 animate-in fade-in slide-in-from-top-2 duration-200">
{solutions.map(solution => (
<button
<a
key={solution.path}
onClick={() => {
onNavigate?.(solution.path)
setIsSolutionsOpen(false)
}}
className="w-full text-left px-5 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
href={`/${solution.path}`}
className="block w-full text-left px-5 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
>
{solution.name}
</button>
</a>
))}
</div>
</div>
@@ -102,39 +82,36 @@ export function Header({ onNavigate }: HeaderProps) {
<div className="absolute top-full left-0 pt-2">
<div className="w-80 bg-white rounded-xl shadow-xl border border-gray-100 py-3 animate-in fade-in slide-in-from-top-2 duration-200">
{services.map(service => (
<button
<a
key={service.path}
onClick={() => {
onNavigate?.(service.path)
setIsServicesOpen(false)
}}
className="w-full text-left px-5 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
href={`/${service.path}`}
className="block w-full text-left px-5 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors"
>
{service.name}
</button>
</a>
))}
</div>
</div>
)}
</div>
<a
href="#cases"
onClick={e => handleNavClick(e, 'cases')}
href="/#cases"
className="text-gray-700 hover:text-blue-600 transition-colors whitespace-nowrap"
onClick={e => navigateHomeSectionAnchor(e, 'cases')}
>
Кейсы
</a>
<a
href="#about"
onClick={e => handleNavClick(e, 'about')}
href="/#about"
className="text-gray-700 hover:text-blue-600 transition-colors whitespace-nowrap"
onClick={e => navigateHomeSectionAnchor(e, 'about')}
>
О компании
</a>
<a
href="#contact"
onClick={e => handleNavClick(e, 'contact')}
href="/#contact"
className="text-gray-700 hover:text-blue-600 transition-colors whitespace-nowrap"
onClick={e => navigateHomeSectionAnchor(e, 'contact')}
>
Контакты
</a>
@@ -150,7 +127,7 @@ export function Header({ onNavigate }: HeaderProps) {
</button>
</div>
<button onClick={() => setIsMenuOpen(!isMenuOpen)} className="lg:hidden p-2">
<button type="button" onClick={() => setIsMenuOpen(!isMenuOpen)} className="lg:hidden p-2">
{isMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
@@ -160,6 +137,7 @@ export function Header({ onNavigate }: HeaderProps) {
<div className="flex flex-col gap-4">
<div>
<button
type="button"
onClick={() => setIsSolutionsOpen(!isSolutionsOpen)}
className="text-gray-700 hover:text-blue-600 w-full text-left flex items-center justify-between"
>
@@ -169,57 +147,74 @@ export function Header({ onNavigate }: HeaderProps) {
{isSolutionsOpen && (
<div className="pl-4 mt-2 space-y-2">
{solutions.map(solution => (
<button
<a
key={solution.path}
href={`/${solution.path}`}
onClick={() => {
onNavigate?.(solution.path)
setIsMenuOpen(false)
setIsSolutionsOpen(false)
}}
className="block w-full text-left text-sm text-gray-600 hover:text-blue-600 py-2"
>
{solution.name}
</button>
</a>
))}
</div>
)}
</div>
<div>
<button
type="button"
onClick={() => setIsServicesOpen(!isServicesOpen)}
className="text-gray-700 hover:text-blue-600 w-full text-left flex items-center justify-between"
>
Услуги
<ChevronDown className={`w-4 h-4 transition-transform ${isSolutionsOpen ? 'rotate-180' : ''}`} />
<ChevronDown className={`w-4 h-4 transition-transform ${isServicesOpen ? 'rotate-180' : ''}`} />
</button>
{isServicesOpen && (
<div className="pl-4 mt-2 space-y-2">
{services.map(service => (
<button
<a
key={service.path}
href={`/${service.path}`}
onClick={() => {
onNavigate?.(service.path)
setIsMenuOpen(false)
setIsSolutionsOpen(false)
setIsServicesOpen(false)
}}
className="block w-full text-left text-sm text-gray-600 hover:text-blue-600 py-2"
>
{service.name}
</button>
</a>
))}
</div>
)}
</div>
<a href="#cases" onClick={e => handleNavClick(e, 'cases')} className="text-gray-700 hover:text-blue-600">
<a
href="/#cases"
onClick={e => {
navigateHomeSectionAnchor(e, 'cases')
setIsMenuOpen(false)
}}
className="text-gray-700 hover:text-blue-600"
>
Кейсы
</a>
<a href="#about" onClick={e => handleNavClick(e, 'about')} className="text-gray-700 hover:text-blue-600">
<a
href="/#about"
onClick={e => {
navigateHomeSectionAnchor(e, 'about')
setIsMenuOpen(false)
}}
className="text-gray-700 hover:text-blue-600"
>
О компании
</a>
<a
href="#contact"
onClick={e => handleNavClick(e, 'contact')}
href="/#contact"
onClick={e => {
navigateHomeSectionAnchor(e, 'contact')
setIsMenuOpen(false)
}}
className="text-gray-700 hover:text-blue-600"
>
Контакты

1
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="astro/client" />

27
src/layouts/Layout.astro Normal file
View File

@@ -0,0 +1,27 @@
---
import ClientRouter from 'astro/components/ClientRouter.astro'
interface Props {
title?: string
description?: string
}
const {
title = 'SOFTCLICK - Российские IT-решения с 2021 года',
description = 'SOFTCLICK - российская IT-компания, специализирующаяся на импортозамещении и внедрении отечественных решений',
} = Astro.props
---
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title}</title>
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>

51
src/lib/scrollToHash.ts Normal file
View File

@@ -0,0 +1,51 @@
import type { MouseEvent } from 'react'
const HEADER_OFFSET_PX = 85
function prefersReducedMotion() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
}
/** Smooth scroll to an element id (fixed header offset). Safe to call from click handlers on `/`. */
export function scrollToHashTarget(id: string) {
if (!id) return
let attempts = 0
const maxAttempts = 24
const tryScroll = () => {
const el = document.getElementById(id)
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - HEADER_OFFSET_PX
const y = Math.max(0, top)
const behavior = prefersReducedMotion() ? ('auto' as const) : ('smooth' as const)
// Double rAF: smooth scroll often fails to animate when run in the same frame as layout/hydration
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
window.scrollTo({ left: 0, top: y, behavior })
})
})
return
}
if (attempts < maxAttempts) {
attempts += 1
requestAnimationFrame(tryScroll)
}
}
requestAnimationFrame(tryScroll)
}
/** On the home page only: prevent the browser’s instant # jump, update the URL, then smooth-scroll. */
export function navigateHomeSectionAnchor(e: MouseEvent<HTMLAnchorElement>, id: string) {
if (typeof window === 'undefined') return
const path = window.location.pathname
const isHome = path === '/' || path === ''
if (!isHome) return
e.preventDefault()
const nextHash = `#${id}`
if (window.location.hash !== nextHash) {
window.history.replaceState(null, '', nextHash)
}
scrollToHashTarget(id)
}

View File

@@ -1,11 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './styles/globals.css'
import './styles/animations.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

View File

@@ -1,29 +0,0 @@
import { Hero } from '../components/Hero'
import { Services } from '../components/Services'
import { Solutions } from '../components/Solutions'
import { Cases } from '../components/Cases'
import { About } from '../components/About'
import { Contact } from '../components/Contact'
interface HomePageProps {
onOpenContactModal?: () => void
}
export function HomePage({ onOpenContactModal }: HomePageProps) {
return (
<>
<Hero onOpenContactModal={onOpenContactModal} />
<Services />
<Solutions />
<div id="cases">
<Cases />
</div>
<div id="about">
<About />
</div>
<div id="contact">
<Contact />
</div>
</>
)
}

39
src/pages/[slug].astro Normal file
View File

@@ -0,0 +1,39 @@
---
import Layout from '../layouts/Layout.astro'
import '../styles/globals.css'
import { Header } from '../components/Header'
import { Footer } from '../components/Footer'
import { CategoryRoute } from '../views/CategoryRoute'
export function getStaticPaths() {
return [
{ params: { slug: 'cloud' } },
{ params: { slug: 'russian-software' } },
{ params: { slug: 'cybersecurity' } },
{ params: { slug: 'ai-solutions' } },
{ params: { slug: 'russian-hardware' } },
{ params: { slug: 'hardware-solutions' } },
{ params: { slug: 'cad-gis' } },
{ params: { slug: 'industrial-solutions' } },
{ params: { slug: 'government' } },
{ params: { slug: 'business' } },
{ params: { slug: 'consulting' } },
{ params: { slug: 'implementation' } },
{ params: { slug: 'managed-services' } },
{ params: { slug: 'integration' } },
{ params: { slug: 'specialized-services' } },
{ params: { slug: 'support' } },
{ params: { slug: 'subscription' } },
]
}
const slug = Astro.params.slug!
---
<Layout>
<div class="min-h-screen bg-white w-full overflow-x-hidden">
<Header client:load />
<CategoryRoute client:load slug={slug} />
<Footer client:load />
</div>
</Layout>

15
src/pages/index.astro Normal file
View File

@@ -0,0 +1,15 @@
---
import Layout from '../layouts/Layout.astro'
import '../styles/globals.css'
import { Header } from '../components/Header'
import { Footer } from '../components/Footer'
import { HomePage } from '../views/HomePage'
---
<Layout>
<div class="min-h-screen bg-white w-full overflow-x-hidden">
<Header client:load />
<HomePage client:load />
<Footer client:load />
</div>
</Layout>

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface AISolutionsPageProps {
onBack: () => void
}
export function AISolutionsPage({ onBack }: AISolutionsPageProps) {
export function AISolutionsPage() {
const vendors = [
{
name: 'Яндекс AI',
@@ -58,7 +55,6 @@ export function AISolutionsPage({ onBack }: AISolutionsPageProps) {
title="Решения на базе ИИ"
description="Передовые технологии искусственного интеллекта и машинного обучения для автоматизации бизнес-процессов"
vendors={vendors}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface BusinessPageProps {
onBack: () => void
}
export function BusinessPage({ onBack }: BusinessPageProps) {
export function BusinessPage() {
const vendors = [
{
name: 'SAP',
@@ -58,7 +55,6 @@ export function BusinessPage({ onBack }: BusinessPageProps) {
title="Решения для бизнеса"
description="Комплексные IT-решения для автоматизации бизнес-процессов, CRM, ERP и управления предприятием"
vendors={vendors}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface CADandGISPageProps {
onBack: () => void
}
export function CADandGISPage({ onBack }: CADandGISPageProps) {
export function CADandGISPage() {
const vendors = [
{
name: 'АСКОН',
@@ -58,7 +55,6 @@ export function CADandGISPage({ onBack }: CADandGISPageProps) {
title="САПР и ГИС"
description="Системы автоматизированного проектирования и геоинформационные системы для инженеров, архитекторов и проектировщиков"
vendors={vendors}
onBack={onBack}
/>
)
}

View File

@@ -0,0 +1,58 @@
import { AISolutionsPage } from './AISolutionsPage'
import { BusinessPage } from './BusinessPage'
import { CADandGISPage } from './CADandGISPage'
import { CloudPage } from './CloudPage'
import { ConsultingPage } from './ConsultingPage'
import { CybersecurityPage } from './CybersecurityPage'
import { GovernmentPage } from './GovernmentPage'
import { HardwareSolutionsPage } from './HardwareSolutionsPage'
import { ImplementationPage } from './ImplementationPage'
import { IndustrialSolutionsPage } from './IndustrialSolutionsPage'
import { IntegrationPage } from './IntegrationPage'
import { ManagedServicesPage } from './ManagedServicesPage'
import { RussianHardwarePage } from './RussianHardwarePage'
import { RussianSoftwarePage } from './RussianSoftwarePage'
import { SpecializedServicesPage } from './SpecializedServicesPage'
import { SubscriptionPage } from './SubscriptionPage'
import { SupportPage } from './SupportPage'
export function CategoryRoute({ slug }: { slug: string }) {
switch (slug) {
case 'cloud':
return <CloudPage />
case 'russian-software':
return <RussianSoftwarePage />
case 'cybersecurity':
return <CybersecurityPage />
case 'ai-solutions':
return <AISolutionsPage />
case 'russian-hardware':
return <RussianHardwarePage />
case 'hardware-solutions':
return <HardwareSolutionsPage />
case 'cad-gis':
return <CADandGISPage />
case 'industrial-solutions':
return <IndustrialSolutionsPage />
case 'government':
return <GovernmentPage />
case 'business':
return <BusinessPage />
case 'consulting':
return <ConsultingPage />
case 'implementation':
return <ImplementationPage />
case 'managed-services':
return <ManagedServicesPage />
case 'integration':
return <IntegrationPage />
case 'specialized-services':
return <SpecializedServicesPage />
case 'support':
return <SupportPage />
case 'subscription':
return <SubscriptionPage />
default:
return null
}
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface CloudPageProps {
onBack: () => void
}
export function CloudPage({ onBack }: CloudPageProps) {
export function CloudPage() {
const vendors = [
{
name: 'Яндекс.Облако',
@@ -43,7 +40,6 @@ export function CloudPage({ onBack }: CloudPageProps) {
title="Облачные решения"
description="Российские облачные платформы для размещения инфраструктуры, приложений и данных с соблюдением требований законодательства РФ"
vendors={vendors}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface ConsultingPageProps {
onBack: () => void
}
export function ConsultingPage({ onBack }: ConsultingPageProps) {
export function ConsultingPage() {
const services = [
{
name: 'IT-стратегия и архитектура',
@@ -47,7 +44,6 @@ export function ConsultingPage({ onBack }: ConsultingPageProps) {
title="Консалтинг и проектирование"
description="Экспертный консалтинг по построению и трансформации IT-инфраструктуры. Помогаем разработать стратегию цифровизации, выбрать оптимальные технологии и спланировать миграцию на российские решения."
vendors={services}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface CybersecurityPageProps {
onBack: () => void
}
export function CybersecurityPage({ onBack }: CybersecurityPageProps) {
export function CybersecurityPage() {
const vendors = [
{
name: 'Лаборатория Касперского',
@@ -58,7 +55,6 @@ export function CybersecurityPage({ onBack }: CybersecurityPageProps) {
title="Кибербезопасность"
description="Комплексные решения для защиты корпоративной инфраструктуры, данных и приложений от современных киберугроз"
vendors={vendors}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface GovernmentPageProps {
onBack: () => void
}
export function GovernmentPage({ onBack }: GovernmentPageProps) {
export function GovernmentPage() {
const vendors = [
{
name: 'Astra Linux',
@@ -58,7 +55,6 @@ export function GovernmentPage({ onBack }: GovernmentPageProps) {
title="Для госсектора"
description="Специализированные решения для государственных органов с соблюдением требований ФСТЭК и ФСБ"
vendors={vendors}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface HardwareSolutionsPageProps {
onBack: () => void
}
export function HardwareSolutionsPage({ onBack }: HardwareSolutionsPageProps) {
export function HardwareSolutionsPage() {
const vendors = [
{
name: 'YADRO',
@@ -58,7 +55,6 @@ export function HardwareSolutionsPage({ onBack }: HardwareSolutionsPageProps) {
title="Аппаратные решения"
description="Комплексные аппаратные решения для построения IT-инфраструктуры: СХД, сетевое оборудование, компоненты"
vendors={vendors}
onBack={onBack}
/>
)
}

41
src/views/HomePage.tsx Normal file
View File

@@ -0,0 +1,41 @@
import { useEffect, useState } from 'react'
import { Hero } from '../components/Hero'
import { Services } from '../components/Services'
import { Solutions } from '../components/Solutions'
import { Cases } from '../components/Cases'
import { About } from '../components/About'
import { Contact } from '../components/Contact'
import { ContactModal } from '../components/ContactModal'
import { scrollToHashTarget } from '../lib/scrollToHash'
const HEADER_OFFSET_PX = 85
export function HomePage() {
const [contactOpen, setContactOpen] = useState(false)
useEffect(() => {
const onHashChange = () => scrollToHashTarget(window.location.hash.slice(1))
onHashChange()
window.addEventListener('hashchange', onHashChange)
return () => window.removeEventListener('hashchange', onHashChange)
}, [])
return (
<>
<Hero onOpenContactModal={() => setContactOpen(true)} />
<Services />
<Solutions />
<div id="cases" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
<Cases />
</div>
<div id="about" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
<About />
</div>
<div id="contact" style={{ scrollMarginTop: HEADER_OFFSET_PX }}>
<Contact />
</div>
<ContactModal isOpen={contactOpen} onClose={() => setContactOpen(false)} />
</>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface ImplementationPageProps {
onBack: () => void
}
export function ImplementationPage({ onBack }: ImplementationPageProps) {
export function ImplementationPage() {
const services = [
{
name: 'Внедрение облачных платформ',
@@ -44,7 +41,6 @@ export function ImplementationPage({ onBack }: ImplementationPageProps) {
title="Поставка и внедрение решений"
description="Полный цикл поставки и внедрения IT-решений: от подбора оборудования и ПО до установки, настройки и ввода в эксплуатацию. Работаем только с сертифицированными российскими решениями."
vendors={services}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface IndustrialSolutionsPageProps {
onBack: () => void
}
export function IndustrialSolutionsPage({ onBack }: IndustrialSolutionsPageProps) {
export function IndustrialSolutionsPage() {
const vendors = [
{
name: 'ОВЕН',
@@ -58,7 +55,6 @@ export function IndustrialSolutionsPage({ onBack }: IndustrialSolutionsPageProps
title="Индустриальные решения"
description="Системы промышленной автоматизации, SCADA, MES и решения для управления технологическими процессами"
vendors={vendors}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface IntegrationPageProps {
onBack: () => void
}
export function IntegrationPage({ onBack }: IntegrationPageProps) {
export function IntegrationPage() {
const services = [
{
name: 'Интеграция корпоративных систем',
@@ -45,7 +42,6 @@ export function IntegrationPage({ onBack }: IntegrationPageProps) {
title="Интеграция и разработка"
description="Разрабатываем и интегрируем ПО любой сложности. От интеграции корпоративных систем до создания уникальных решений с нуля на современных технологических стеках."
vendors={services}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface ManagedServicesPageProps {
onBack: () => void
}
export function ManagedServicesPage({ onBack }: ManagedServicesPageProps) {
export function ManagedServicesPage() {
const services = [
{
name: 'Управление инфраструктурой',
@@ -44,7 +41,6 @@ export function ManagedServicesPage({ onBack }: ManagedServicesPageProps) {
title="Управляемые сервисы"
description="Передайте управление IT-инфраструктурой профессионалам. Обеспечиваем круглосуточный мониторинг, поддержку и развитие ваших IT-систем с гарантией SLA."
vendors={services}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface RussianHardwarePageProps {
onBack: () => void
}
export function RussianHardwarePage({ onBack }: RussianHardwarePageProps) {
export function RussianHardwarePage() {
const vendors = [
{
name: 'Kraftway',
@@ -58,7 +55,6 @@ export function RussianHardwarePage({ onBack }: RussianHardwarePageProps) {
title="Российские ноутбуки, ПК и серверы"
description="Отечественное компьютерное и серверное оборудование для корпоративного сектора и государственных организаций"
vendors={vendors}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface RussianSoftwarePageProps {
onBack: () => void
}
export function RussianSoftwarePage({ onBack }: RussianSoftwarePageProps) {
export function RussianSoftwarePage() {
const vendors = [
{
name: 'МойОфис',
@@ -58,7 +55,6 @@ export function RussianSoftwarePage({ onBack }: RussianSoftwarePageProps) {
title="Российское ПО"
description="Комплексные программные решения от российских разработчиков для бизнеса, образования и государственного сектора"
vendors={vendors}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface SpecializedServicesPageProps {
onBack: () => void
}
export function SpecializedServicesPage({ onBack }: SpecializedServicesPageProps) {
export function SpecializedServicesPage() {
const services = [
{
name: 'Решения для финансового сектора',
@@ -44,7 +41,6 @@ export function SpecializedServicesPage({ onBack }: SpecializedServicesPageProps
title="Специализированные услуги по отраслям"
description="Отраслевая экспертиза в финансах, промышленности, здравоохранении, образовании, ритейле и телекоме. Понимаем специфику бизнеса и требования регуляторов."
vendors={services}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface SubscriptionPageProps {
onBack: () => void
}
export function SubscriptionPage({ onBack }: SubscriptionPageProps) {
export function SubscriptionPage() {
const services = [
{
name: 'Облачная инфраструктура по подписке',
@@ -43,7 +40,6 @@ export function SubscriptionPage({ onBack }: SubscriptionPageProps) {
title="Услуги по подписке"
description="Современная модель потребления IT-услуг. Платите только за использованные ресурсы, получайте актуальные обновления и профессиональную поддержку без капитальных затрат."
vendors={services}
onBack={onBack}
/>
)
}

View File

@@ -1,10 +1,7 @@
import { CategoryPage } from '../components/CategoryPage'
interface SupportPageProps {
onBack: () => void
}
export function SupportPage({ onBack }: SupportPageProps) {
export function SupportPage() {
const services = [
{
name: 'Техническая поддержка 24/7',
@@ -43,7 +40,6 @@ export function SupportPage({ onBack }: SupportPageProps) {
title="Обслуживание и сопровождение"
description="Надежная техническая поддержка и обслуживание IT-инфраструктуры. Гарантируем стабильную работу всех систем с минимальным временем простоя."
vendors={services}
onBack={onBack}
/>
)
}

1
src/vite-env.d.ts vendored
View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,19 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -1,7 +1,8 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
},
"include": ["src", ".astro/types.d.ts"]
}

View File

@@ -1,11 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler"
},
"include": ["vite.config.ts"]
}

View File

@@ -1,8 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
});