Initial commit: Сисадмингрупп website

React + Vite + Tailwind, RU/EN, Hero/Services/About/Contact sections
This commit is contained in:
2026-03-14 01:02:02 +03:00
parent b3cb94d01b
commit abff3fa4cc
19 changed files with 2834 additions and 0 deletions

17
src/App.jsx Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react'
import { LanguageProvider } from './contexts/LanguageContext.jsx'
import Navigation from './components/Navigation.jsx'
import Footer from './components/Footer.jsx'
import Home from './pages/Home.jsx'
export default function App() {
return (
<LanguageProvider>
<Navigation />
<main>
<Home />
</main>
<Footer />
</LanguageProvider>
)
}

20
src/components/Footer.jsx Normal file
View File

@@ -0,0 +1,20 @@
import React from 'react'
import { useLanguage } from '../contexts/LanguageContext.jsx'
export default function Footer() {
const { t } = useLanguage()
const year = new Date().getFullYear()
return (
<footer className="bg-slate-900 text-slate-400 py-10">
<div className="max-w-6xl mx-auto px-4 sm:px-6 flex flex-col md:flex-row items-center justify-between gap-4">
<span className="text-white font-semibold">Сисадмингрупп</span>
<span className="text-sm">{year} © {t('footer.rights')}</span>
<div className="flex gap-6 text-sm">
<a href="tel:+74953637476" className="hover:text-white transition-colors">+7 495 363-74-76</a>
<a href="mailto:info@sag24.ru" className="hover:text-white transition-colors">info@sag24.ru</a>
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,70 @@
import React, { useState, useEffect } from 'react'
import { Menu, X } from 'lucide-react'
import { useLanguage } from '../contexts/LanguageContext.jsx'
export default function Navigation() {
const { t, lang, toggle } = useLanguage()
const [scrolled, setScrolled] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 40)
window.addEventListener('scroll', onScroll)
return () => window.removeEventListener('scroll', onScroll)
}, [])
const links = [
{ label: t('nav.services'), href: '#services' },
{ label: t('nav.about'), href: '#about' },
{ label: t('nav.contact'), href: '#contact' },
]
return (
<header className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${scrolled ? 'bg-white/95 backdrop-blur shadow-sm' : 'bg-transparent'}`}>
<div className="max-w-6xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between">
<a href="#" className="text-xl font-bold text-slate-900 tracking-tight">
Сисадмингрупп
</a>
{/* Desktop nav */}
<nav className="hidden md:flex items-center gap-8">
{links.map(l => (
<a key={l.href} href={l.href} className="text-sm font-medium text-slate-600 hover:text-blue-700 transition-colors">
{l.label}
</a>
))}
<button
onClick={toggle}
className="text-sm font-semibold text-slate-500 hover:text-blue-700 transition-colors border border-slate-200 rounded px-2 py-0.5"
>
{lang === 'ru' ? 'EN' : 'RU'}
</button>
<a href="#contact" className="px-4 py-2 bg-blue-700 hover:bg-blue-800 text-white text-sm font-semibold rounded transition-colors">
{t('nav.contact')}
</a>
</nav>
{/* Mobile */}
<div className="md:hidden flex items-center gap-3">
<button onClick={toggle} className="text-sm font-semibold text-slate-500 border border-slate-200 rounded px-2 py-0.5">
{lang === 'ru' ? 'EN' : 'RU'}
</button>
<button onClick={() => setMenuOpen(!menuOpen)} className="p-1 text-slate-700">
{menuOpen ? <X size={22} /> : <Menu size={22} />}
</button>
</div>
</div>
{/* Mobile menu */}
{menuOpen && (
<div className="md:hidden bg-white border-t border-slate-100 px-4 py-4 flex flex-col gap-4">
{links.map(l => (
<a key={l.href} href={l.href} onClick={() => setMenuOpen(false)} className="text-sm font-medium text-slate-700 hover:text-blue-700">
{l.label}
</a>
))}
</div>
)}
</header>
)
}

View File

@@ -0,0 +1,18 @@
import { useEffect, useRef } from 'react'
export function useReveal() {
const ref = useRef(null)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => { if (entry.isIntersecting) el.classList.add('visible') },
{ threshold: 0.15 }
)
observer.observe(el)
return () => observer.disconnect()
}, [])
return ref
}

View File

@@ -0,0 +1,37 @@
import React, { createContext, useContext, useState } from 'react'
import { ru, en } from '../translations/index.js'
const translations = { ru, en }
const LanguageContext = createContext(null)
export function LanguageProvider({ children }) {
const [lang, setLang] = useState(() => {
return localStorage.getItem('lang') || 'ru'
})
const toggle = () => {
const next = lang === 'ru' ? 'en' : 'ru'
setLang(next)
localStorage.setItem('lang', next)
}
const t = (key) => {
const keys = key.split('.')
let value = translations[lang]
for (const k of keys) {
value = value?.[k]
}
return value ?? key
}
return (
<LanguageContext.Provider value={{ lang, toggle, t }}>
{children}
</LanguageContext.Provider>
)
}
export function useLanguage() {
return useContext(LanguageContext)
}

31
src/index.css Normal file
View File

@@ -0,0 +1,31 @@
@import '@fontsource/manrope/400.css';
@import '@fontsource/manrope/500.css';
@import '@fontsource/manrope/600.css';
@import '@fontsource/manrope/700.css';
@import '@fontsource/manrope/800.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
}
body {
font-family: 'Manrope', sans-serif;
@apply bg-white text-slate-900;
}
}
@layer components {
.section-reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.section-reveal.visible {
opacity: 1;
transform: translateY(0);
}
}

10
src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

195
src/pages/Home.jsx Normal file
View File

@@ -0,0 +1,195 @@
import React, { useState } from 'react'
import { ChevronRight, Server, Shield, Headphones, Phone, Mail, MapPin, CheckCircle2 } from 'lucide-react'
import { useLanguage } from '../contexts/LanguageContext.jsx'
import { useReveal } from '../components/useReveal.js'
const serviceIcons = [Server, Shield, Headphones]
function ServiceCard({ icon: Icon, title, description, points }) {
const ref = useReveal()
return (
<div ref={ref} className="section-reveal bg-white border border-slate-200 rounded-xl p-8 hover:shadow-lg hover:border-blue-200 transition-all duration-300 flex flex-col gap-4">
<div className="w-12 h-12 bg-blue-50 rounded-lg flex items-center justify-center">
<Icon size={24} className="text-blue-700" />
</div>
<h3 className="text-xl font-bold text-slate-900">{title}</h3>
<p className="text-slate-600 text-sm leading-relaxed">{description}</p>
<ul className="flex flex-col gap-2 mt-auto pt-4 border-t border-slate-100">
{points.map((p, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-slate-700">
<CheckCircle2 size={16} className="text-blue-600 flex-shrink-0" />
{p}
</li>
))}
</ul>
</div>
)
}
export default function Home() {
const { t } = useLanguage()
const [formState, setFormState] = useState({ name: '', company: '', message: '' })
const [submitted, setSubmitted] = useState(false)
const aboutRef = useReveal()
const contactRef = useReveal()
const services = t('services.items')
const stats = [t('about.stat1'), t('about.stat2'), t('about.stat3')]
const handleSubmit = (e) => {
e.preventDefault()
setSubmitted(true)
}
return (
<div>
{/* Hero */}
<section className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-blue-950 to-slate-900 relative overflow-hidden">
<div className="absolute inset-0 opacity-20">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-500 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-blue-700 rounded-full blur-3xl" />
</div>
<div className="max-w-4xl mx-auto px-4 sm:px-6 py-32 text-center relative z-10">
<div className="inline-flex items-center gap-2 bg-blue-500/10 border border-blue-500/20 rounded-full px-4 py-1.5 mb-8">
<div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse" />
<span className="text-blue-300 text-sm font-medium">IT-аутсорсинг · Безопасность · Поддержка</span>
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-white leading-tight mb-6">
{t('hero.title')}
</h1>
<p className="text-lg sm:text-xl text-slate-300 max-w-2xl mx-auto mb-10 leading-relaxed">
{t('hero.subtitle')}
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a href="#contact" className="px-8 py-4 bg-blue-600 hover:bg-blue-500 text-white font-semibold rounded-lg transition-all duration-300 flex items-center justify-center gap-2 group">
{t('hero.cta')}
<ChevronRight size={20} className="group-hover:translate-x-1 transition-transform" />
</a>
<a href="#services" className="px-8 py-4 border border-white/20 hover:border-white/40 text-white font-semibold rounded-lg transition-all duration-300">
{t('hero.ctaSecondary')}
</a>
</div>
</div>
</section>
{/* Services */}
<section id="services" className="py-24 bg-slate-50">
<div className="max-w-6xl mx-auto px-4 sm:px-6">
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl md:text-5xl font-bold text-slate-900 mb-4">{t('services.title')}</h2>
<p className="text-slate-500 text-lg max-w-2xl mx-auto">{t('services.subtitle')}</p>
</div>
<div className="grid md:grid-cols-3 gap-6">
{services.map((svc, i) => (
<ServiceCard key={i} icon={serviceIcons[i]} {...svc} />
))}
</div>
</div>
</section>
{/* About */}
<section id="about" className="py-24 bg-white">
<div className="max-w-6xl mx-auto px-4 sm:px-6">
<div ref={aboutRef} className="section-reveal grid md:grid-cols-2 gap-16 items-center">
<div>
<h2 className="text-3xl sm:text-4xl font-bold text-slate-900 mb-6">{t('about.title')}</h2>
<p className="text-slate-600 text-lg leading-relaxed mb-4">{t('about.text1')}</p>
<p className="text-slate-600 leading-relaxed">{t('about.text2')}</p>
</div>
<div className="grid grid-cols-3 gap-4">
{stats.map((s, i) => (
<div key={i} className="bg-slate-50 rounded-xl p-6 text-center border border-slate-100">
<div className="text-3xl font-bold text-blue-700 mb-1">{s.value}</div>
<div className="text-sm text-slate-500">{s.label}</div>
</div>
))}
</div>
</div>
</div>
</section>
{/* Contact */}
<section id="contact" className="py-24 bg-slate-900">
<div className="max-w-6xl mx-auto px-4 sm:px-6">
<div ref={contactRef} className="section-reveal grid md:grid-cols-2 gap-16">
<div>
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">{t('contact.title')}</h2>
<p className="text-slate-400 text-lg mb-10">{t('contact.subtitle')}</p>
<div className="flex flex-col gap-6">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-500/10 rounded-lg flex items-center justify-center flex-shrink-0">
<Phone size={20} className="text-blue-400" />
</div>
<div>
<div className="text-slate-400 text-xs uppercase tracking-wider mb-0.5">{t('contact.phone')}</div>
<a href="tel:+74953637476" className="text-white font-medium hover:text-blue-400 transition-colors">+7 495 363-74-76</a>
</div>
</div>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-500/10 rounded-lg flex items-center justify-center flex-shrink-0">
<Mail size={20} className="text-blue-400" />
</div>
<div>
<div className="text-slate-400 text-xs uppercase tracking-wider mb-0.5">{t('contact.email')}</div>
<a href="mailto:info@sag24.ru" className="text-white font-medium hover:text-blue-400 transition-colors">info@sag24.ru</a>
</div>
</div>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-500/10 rounded-lg flex items-center justify-center flex-shrink-0">
<MapPin size={20} className="text-blue-400" />
</div>
<div>
<div className="text-slate-400 text-xs uppercase tracking-wider mb-0.5">{t('contact.address')}</div>
<span className="text-white font-medium">{t('contact.addressValue')}</span>
</div>
</div>
</div>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-8">
{submitted ? (
<div className="flex flex-col items-center justify-center h-full gap-4 py-8">
<CheckCircle2 size={48} className="text-green-400" />
<p className="text-white text-center text-lg">{t('contact.formSuccess')}</p>
</div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<input
type="text"
required
placeholder={t('contact.formName')}
value={formState.name}
onChange={e => setFormState({ ...formState, name: e.target.value })}
className="w-full bg-white/10 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 transition-colors text-sm"
/>
<input
type="text"
placeholder={t('contact.formCompany')}
value={formState.company}
onChange={e => setFormState({ ...formState, company: e.target.value })}
className="w-full bg-white/10 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 transition-colors text-sm"
/>
<textarea
required
rows={5}
placeholder={t('contact.formMessage')}
value={formState.message}
onChange={e => setFormState({ ...formState, message: e.target.value })}
className="w-full bg-white/10 border border-white/10 rounded-lg px-4 py-3 text-white placeholder-slate-400 focus:outline-none focus:border-blue-500 transition-colors text-sm resize-none"
/>
<button
type="submit"
className="w-full py-3 bg-blue-600 hover:bg-blue-500 text-white font-semibold rounded-lg transition-colors"
>
{t('contact.formSubmit')}
</button>
</form>
)}
</div>
</div>
</div>
</section>
</div>
)
}

58
src/translations/en.js Normal file
View File

@@ -0,0 +1,58 @@
export const en = {
nav: {
services: 'Services',
about: 'About',
contact: 'Contact',
},
hero: {
title: 'IT Solutions for Your Business',
subtitle: 'We take full responsibility for your IT infrastructure — from workstation support to data protection',
cta: 'Discuss a Project',
ctaSecondary: 'Learn More',
},
services: {
title: 'Our Services',
subtitle: 'End-to-end IT infrastructure management',
items: [
{
title: 'IT Outsourcing',
description: 'Full management of your IT infrastructure: servers, networks, workstations. Fast response, results-driven.',
points: ['24/7 support', 'Remote and on-site service', 'Fixed monthly cost'],
},
{
title: 'Cybersecurity',
description: 'Corporate data protection, security audits, firewall and antivirus configuration.',
points: ['Vulnerability audit & analysis', 'Firewall & VPN setup', 'Data loss prevention'],
},
{
title: 'Technical Support',
description: 'Fast resolution of employee technical issues. Help desk, ticket management, user training.',
points: ['Help desk & ticketing', 'User training', 'SLA from 15 minutes'],
},
],
},
about: {
title: 'About Us',
text1: 'SysadminGroup is an IT company from Pushkino with years of experience serving small and medium businesses. We handle all IT infrastructure so you can focus on your business.',
text2: 'Our team consists of certified specialists in networking, information security, and system administration.',
stat1: { value: '10+', label: 'years on market' },
stat2: { value: '150+', label: 'clients' },
stat3: { value: '15 min', label: 'response time' },
},
contact: {
title: 'Contact Us',
subtitle: 'Tell us about your challenge — we will propose a solution',
phone: 'Phone',
email: 'Email',
address: 'Address',
addressValue: 'Pushkino, Moscow Region',
formName: 'Your name',
formCompany: 'Company',
formMessage: 'Describe your task',
formSubmit: 'Send Request',
formSuccess: 'Request sent! We will contact you shortly.',
},
footer: {
rights: 'All rights reserved.',
},
}

View File

@@ -0,0 +1,2 @@
export { ru } from './ru.js'
export { en } from './en.js'

58
src/translations/ru.js Normal file
View File

@@ -0,0 +1,58 @@
export const ru = {
nav: {
services: 'Услуги',
about: 'О нас',
contact: 'Контакты',
},
hero: {
title: 'IT-решения для вашего бизнеса',
subtitle: 'Берём на себя всю IT-инфраструктуру — от поддержки рабочих мест до защиты данных',
cta: 'Обсудить проект',
ctaSecondary: 'Узнать больше',
},
services: {
title: 'Наши услуги',
subtitle: 'Комплексное обслуживание IT-инфраструктуры под ключ',
items: [
{
title: 'IT-аутсорсинг',
description: 'Полное обслуживание вашей IT-инфраструктуры: серверы, сети, рабочие станции. Реагируем быстро, работаем на результат.',
points: ['Поддержка 24/7', 'Удалённое и выездное обслуживание', 'Фиксированная стоимость'],
},
{
title: 'Кибербезопасность',
description: 'Защита корпоративных данных, аудит безопасности, настройка межсетевых экранов и антивирусной защиты.',
points: ['Аудит и анализ уязвимостей', 'Настройка Firewall и VPN', 'Защита от утечек данных'],
},
{
title: 'Техническая поддержка',
description: 'Оперативное решение технических проблем сотрудников. Help desk, управление заявками, обучение пользователей.',
points: ['Help desk и тикет-система', 'Обучение пользователей', 'SLA от 15 минут'],
},
],
},
about: {
title: 'О компании',
text1: 'Сисадмингрупп — IT-компания из Пушкино с многолетним опытом обслуживания малого и среднего бизнеса. Мы берём на себя всю IT-инфраструктуру, чтобы вы могли сосредоточиться на бизнесе.',
text2: 'Наша команда — сертифицированные специалисты с опытом в сетевых технологиях, информационной безопасности и системном администрировании.',
stat1: { value: '10+', label: 'лет на рынке' },
stat2: { value: '150+', label: 'клиентов' },
stat3: { value: '15 мин', label: 'время реакции' },
},
contact: {
title: 'Свяжитесь с нами',
subtitle: 'Расскажите о задаче — предложим решение',
phone: 'Телефон',
email: 'Email',
address: 'Адрес',
addressValue: 'Пушкино, Московская область',
formName: 'Ваше имя',
formCompany: 'Компания',
formMessage: 'Опишите задачу',
formSubmit: 'Отправить заявку',
formSuccess: 'Заявка отправлена! Мы свяжемся с вами в ближайшее время.',
},
footer: {
rights: 'Все права защищены.',
},
}