Initial commit: Сисадмингрупп website
React + Vite + Tailwind, RU/EN, Hero/Services/About/Contact sections
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
|
*.local
|
||||||
14
index.html
Normal file
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!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="Сисадмингрупп — IT-аутсорсинг, кибербезопасность и техническая поддержка в Пушкино" />
|
||||||
|
<title>Сисадмингрупп — IT-решения для бизнеса</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2234
package-lock.json
generated
Normal file
2234
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "sag24-website",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/manrope": "^5.2.8",
|
||||||
|
"lucide-react": "^0.263.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"postcss": "^8.4.24",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"vite": "^4.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
5
public/favicon.svg
Normal file
5
public/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||||
|
<rect width="32" height="32" rx="8" fill="#1d4ed8"/>
|
||||||
|
<path d="M8 10h16M8 16h16M8 22h10" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
<circle cx="24" cy="22" r="3" fill="#60a5fa"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 280 B |
17
src/App.jsx
Normal file
17
src/App.jsx
Normal 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
20
src/components/Footer.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
src/components/Navigation.jsx
Normal file
70
src/components/Navigation.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
src/components/useReveal.js
Normal file
18
src/components/useReveal.js
Normal 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
|
||||||
|
}
|
||||||
37
src/contexts/LanguageContext.jsx
Normal file
37
src/contexts/LanguageContext.jsx
Normal 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
31
src/index.css
Normal 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
10
src/main.jsx
Normal 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
195
src/pages/Home.jsx
Normal 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
58
src/translations/en.js
Normal 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.',
|
||||||
|
},
|
||||||
|
}
|
||||||
2
src/translations/index.js
Normal file
2
src/translations/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { ru } from './ru.js'
|
||||||
|
export { en } from './en.js'
|
||||||
58
src/translations/ru.js
Normal file
58
src/translations/ru.js
Normal 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: 'Все права защищены.',
|
||||||
|
},
|
||||||
|
}
|
||||||
22
tailwind.config.js
Normal file
22
tailwind.config.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,jsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
brand: {
|
||||||
|
50: '#eff6ff',
|
||||||
|
100: '#dbeafe',
|
||||||
|
500: '#3b82f6',
|
||||||
|
600: '#2563eb',
|
||||||
|
700: '#1d4ed8',
|
||||||
|
900: '#1e3a8a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
9
vite.config.js
Normal file
9
vite.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user