ready site
This commit is contained in:
235
app/components/About.tsx
Normal file
235
app/components/About.tsx
Normal file
@ -0,0 +1,235 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { History, GraduationCap, Brain, Users, Coins } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { motion, useAnimation, useInView } from 'framer-motion';
|
||||
|
||||
const About = () => {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
const controls = useAnimation();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInView) {
|
||||
controls.start('visible');
|
||||
}
|
||||
}, [isInView, controls]);
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.2,
|
||||
duration: 0.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const imageVariants = {
|
||||
hidden: { scale: 0.8, opacity: 0 },
|
||||
visible: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.8,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const featureVariants = {
|
||||
hidden: { x: -20, opacity: 0 },
|
||||
visible: {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const statsVariants = {
|
||||
hidden: { scale: 0.5, opacity: 0 },
|
||||
visible: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section ref={ref} id="about" className="py-20 bg-gray-50">
|
||||
<motion.div
|
||||
className="container mx-auto px-4"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate={controls}
|
||||
>
|
||||
<motion.div
|
||||
className="max-w-3xl mx-auto text-center mb-16"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
||||
О нашей компании
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Мы предоставляем профессиональные услуги экспертизы с неизменно
|
||||
высоким качеством
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center mb-20">
|
||||
<div className="space-y-8">
|
||||
<motion.div
|
||||
className="flex items-start space-x-4"
|
||||
variants={featureVariants}
|
||||
>
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<History className="h-6 w-6 text-blue-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Более 9 лет на рынке
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Многолетний опыт работы позволяет нам решать задачи любой
|
||||
сложности и гарантировать высокое качество услуг.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex items-start space-x-4"
|
||||
variants={featureVariants}
|
||||
>
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<GraduationCap className="h-6 w-6 text-blue-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Квалифицированные специалисты
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
В нашей организации работают дипломированные специалисты,
|
||||
кандидат технических наук, инженеры с большим стажем работы и
|
||||
квалифицированный сметчик.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex items-start space-x-4"
|
||||
variants={featureVariants}
|
||||
>
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<Brain className="h-6 w-6 text-blue-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Огромный опыт
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Наработан огромный опыт, позволяющий провести экспертизу даже
|
||||
в самых сложных ситуациях.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="relative h-[400px] rounded-lg overflow-hidden shadow-xl"
|
||||
variants={imageVariants}
|
||||
>
|
||||
<Image
|
||||
src="/images/office.jpg"
|
||||
alt="Наш офис"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mt-12">
|
||||
<motion.div
|
||||
className="bg-white rounded-lg p-8 shadow-lg"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<Users className="h-6 w-6 text-blue-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Работаем со всеми
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Работаем как с юридическими, так и с физическими лицами.
|
||||
Индивидуальный подход к каждому клиенту.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="bg-white rounded-lg p-8 shadow-lg"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<Coins className="h-6 w-6 text-blue-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Честные цены
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Не накручиваем цены и не навязываем ненужные допуслуги.
|
||||
Прозрачное ценообразование и понятные условия сотрудничества.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-8 mt-20"
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div className="text-center" variants={statsVariants}>
|
||||
<div className="text-4xl font-bold text-blue-700 mb-2">9+</div>
|
||||
<div className="text-gray-600">лет опыта</div>
|
||||
</motion.div>
|
||||
<motion.div className="text-center" variants={statsVariants}>
|
||||
<div className="text-4xl font-bold text-blue-700 mb-2">500+</div>
|
||||
<div className="text-gray-600">проектов</div>
|
||||
</motion.div>
|
||||
<motion.div className="text-center" variants={statsVariants}>
|
||||
<div className="text-4xl font-bold text-blue-700 mb-2">50+</div>
|
||||
<div className="text-gray-600">экспертов</div>
|
||||
</motion.div>
|
||||
<motion.div className="text-center" variants={statsVariants}>
|
||||
<div className="text-4xl font-bold text-blue-700 mb-2">98%</div>
|
||||
<div className="text-gray-600">довольных клиентов</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
230
app/components/Certificates.tsx
Normal file
230
app/components/Certificates.tsx
Normal file
@ -0,0 +1,230 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { FileText, X, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { motion, AnimatePresence, useInView } from 'framer-motion';
|
||||
|
||||
const certificates = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Свидетельство СРО',
|
||||
description: 'Свидетельство о членстве в саморегулируемой организации',
|
||||
image: '/images/certificates/sro.jpg',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Свидетельство НОПРИЗ',
|
||||
description:
|
||||
'Свидетельство о членстве в Национальном объединении изыскателей и проектировщиков',
|
||||
image: '/images/certificates/nopriz.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
const Certificates = () => {
|
||||
const [selectedCert, setSelectedCert] = useState<number | null>(null);
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.2,
|
||||
delayChildren: 0.3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const modalVariants = {
|
||||
hidden: { opacity: 0, scale: 0.8 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (selectedCert !== null) {
|
||||
setSelectedCert(
|
||||
selectedCert === 0 ? certificates.length - 1 : selectedCert - 1
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (selectedCert !== null) {
|
||||
setSelectedCert(
|
||||
selectedCert === certificates.length - 1 ? 0 : selectedCert + 1
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-20 bg-gray-50" id="certificates">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="max-w-3xl mx-auto text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
||||
Сертификаты и лицензии
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Все необходимые документы, подтверждающие нашу компетенцию и
|
||||
надежность
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{certificates.map((cert, index) => (
|
||||
<motion.div
|
||||
key={cert.id}
|
||||
variants={itemVariants}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
transition: { duration: 0.2 },
|
||||
}}
|
||||
className="bg-white rounded-xl overflow-hidden shadow-md hover:shadow-lg transition-shadow duration-300 cursor-pointer"
|
||||
onClick={() => setSelectedCert(index)}
|
||||
>
|
||||
<div className="relative h-64">
|
||||
<Image
|
||||
src={cert.image}
|
||||
alt={cert.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
className="absolute inset-0 bg-black bg-opacity-20 flex items-center justify-center"
|
||||
>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.2 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
<FileText className="w-10 h-10 text-white" />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
{cert.title}
|
||||
</h3>
|
||||
<p className="text-gray-600">{cert.description}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{selectedCert !== null && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
|
||||
onClick={() => setSelectedCert(null)}
|
||||
>
|
||||
<motion.div
|
||||
variants={modalVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="relative w-full max-w-4xl bg-white rounded-xl overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 z-10"
|
||||
onClick={() => setSelectedCert(null)}
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</Button>
|
||||
|
||||
<div className="relative h-[80vh]">
|
||||
<Image
|
||||
src={certificates[selectedCert].image}
|
||||
alt={certificates[selectedCert].title}
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="absolute inset-y-0 left-0 flex items-center"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-white bg-black bg-opacity-20 hover:bg-opacity-30 rounded-none"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
<ChevronLeft className="h-8 w-8" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="absolute inset-y-0 right-0 flex items-center"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-white bg-black bg-opacity-20 hover:bg-opacity-30 rounded-none"
|
||||
onClick={handleNext}
|
||||
>
|
||||
<ChevronRight className="h-8 w-8" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<motion.div variants={itemVariants} className="mt-16 text-center">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Все наши документы и сертификаты актуальны и действительны
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Certificates;
|
250
app/components/ContactForm.tsx
Normal file
250
app/components/ContactForm.tsx
Normal file
@ -0,0 +1,250 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { Phone, CheckCircle2, HelpCircle, AlertCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { formatPhoneNumber, validatePhoneNumber } from '@/lib/utils';
|
||||
import { sendTelegramNotification } from '@/lib/telegram';
|
||||
|
||||
const ContactForm = () => {
|
||||
const [phone, setPhone] = useState('');
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
|
||||
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const formattedPhone = formatPhoneNumber(e.target.value);
|
||||
setPhone(formattedPhone);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validatePhoneNumber(phone)) {
|
||||
setError('Пожалуйста, введите корректный российский номер телефона');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const success = await sendTelegramNotification(phone, 'Форма контактов');
|
||||
|
||||
if (success) {
|
||||
setIsSubmitted(true);
|
||||
setError('');
|
||||
setTimeout(() => {
|
||||
setIsSubmitted(false);
|
||||
setPhone('');
|
||||
}, 3000);
|
||||
} else {
|
||||
setError(
|
||||
'Произошла ошибка при отправке заявки. Пожалуйста, попробуйте позже.'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(
|
||||
'Произошла ошибка при отправке заявки. Пожалуйста, попробуйте позже.'
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.2,
|
||||
delayChildren: 0.3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const listItemVariants = {
|
||||
hidden: { x: -20, opacity: 0 },
|
||||
visible: {
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-20 bg-white" id="contact-form">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
variants={containerVariants}
|
||||
className="max-w-4xl mx-auto"
|
||||
>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
className="bg-gradient-to-r from-blue-600 to-blue-800 rounded-2xl p-8 md:p-12 shadow-xl relative overflow-hidden"
|
||||
>
|
||||
{/* Фоновый паттерн */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 0.1 }}
|
||||
transition={{ duration: 1 }}
|
||||
className="absolute inset-0"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\")",
|
||||
backgroundSize: '30px 30px',
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<div className="relative flex flex-col md:flex-row items-center justify-between gap-8">
|
||||
{/* Левая часть с текстом */}
|
||||
<motion.div variants={itemVariants} className="flex-1 text-white">
|
||||
<motion.div
|
||||
className="flex items-center gap-3 mb-6"
|
||||
whileHover={{ x: 10 }}
|
||||
>
|
||||
<motion.div
|
||||
whileHover={{ rotate: 360 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<HelpCircle className="h-10 w-10" />
|
||||
</motion.div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold">
|
||||
Нужна консультация?
|
||||
</h2>
|
||||
</motion.div>
|
||||
<p className="text-xl text-blue-100 mb-6">
|
||||
Оставьте свой номер телефона, и наш специалист
|
||||
проконсультирует вас по всем вопросам
|
||||
</p>
|
||||
<motion.ul
|
||||
variants={containerVariants}
|
||||
className="space-y-3 text-blue-100"
|
||||
>
|
||||
<motion.li
|
||||
variants={listItemVariants}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<CheckCircle2 className="h-5 w-5 flex-shrink-0" />
|
||||
<span>Бесплатная консультация</span>
|
||||
</motion.li>
|
||||
<motion.li
|
||||
variants={listItemVariants}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<CheckCircle2 className="h-5 w-5 flex-shrink-0" />
|
||||
<span>Ответим в течение 15 минут</span>
|
||||
</motion.li>
|
||||
<motion.li
|
||||
variants={listItemVariants}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<CheckCircle2 className="h-5 w-5 flex-shrink-0" />
|
||||
<span>Подберём оптимальное решение</span>
|
||||
</motion.li>
|
||||
</motion.ul>
|
||||
</motion.div>
|
||||
|
||||
{/* Правая часть с формой */}
|
||||
<motion.div variants={itemVariants} className="w-full md:w-auto">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
className="bg-white p-6 rounded-xl shadow-lg w-full md:w-80"
|
||||
>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="+7 (999) 999-99-99"
|
||||
value={phone}
|
||||
onChange={handlePhoneChange}
|
||||
className="pl-10"
|
||||
required
|
||||
disabled={isLoading || isSubmitted}
|
||||
/>
|
||||
{error && (
|
||||
<div className="flex items-center gap-1 mt-1 text-red-500 text-sm">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-700 hover:bg-blue-800 text-white"
|
||||
disabled={isLoading || isSubmitted}
|
||||
>
|
||||
{isSubmitted ? (
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
<span>Заявка отправлена</span>
|
||||
</motion.span>
|
||||
) : isLoading ? (
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<span className="animate-spin rounded-full h-5 w-5 border-b-2 border-white" />
|
||||
<span>Отправка...</span>
|
||||
</motion.span>
|
||||
) : (
|
||||
'Получить консультацию'
|
||||
)}
|
||||
</Button>
|
||||
</motion.div>
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
Нажимая кнопку, вы соглашаетесь с{' '}
|
||||
<a
|
||||
href="/privacy-policy"
|
||||
className="text-blue-700 hover:text-blue-800"
|
||||
>
|
||||
политикой конфиденциальности
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactForm;
|
185
app/components/ContactModal.tsx
Normal file
185
app/components/ContactModal.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Phone, CheckCircle2, X, AlertCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { formatPhoneNumber, validatePhoneNumber } from '@/lib/utils';
|
||||
import { sendTelegramNotification } from '@/lib/telegram';
|
||||
|
||||
interface ContactModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ContactModal = ({ isOpen, onClose }: ContactModalProps) => {
|
||||
const [phone, setPhone] = useState('');
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const formattedPhone = formatPhoneNumber(e.target.value);
|
||||
setPhone(formattedPhone);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validatePhoneNumber(phone)) {
|
||||
setError('Пожалуйста, введите корректный российский номер телефона');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const success = await sendTelegramNotification(phone, 'Модальное окно');
|
||||
|
||||
if (success) {
|
||||
setIsSubmitted(true);
|
||||
setError('');
|
||||
setTimeout(() => {
|
||||
setIsSubmitted(false);
|
||||
setPhone('');
|
||||
onClose();
|
||||
}, 2000);
|
||||
} else {
|
||||
setError(
|
||||
'Произошла ошибка при отправке заявки. Пожалуйста, попробуйте позже.'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(
|
||||
'Произошла ошибка при отправке заявки. Пожалуйста, попробуйте позже.'
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const modalVariants = {
|
||||
hidden: { opacity: 0, scale: 0.8 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
variants={modalVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="bg-white rounded-xl p-6 w-full max-w-md relative"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-2 top-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-5 w-5 text-gray-500" />
|
||||
</Button>
|
||||
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Оставить заявку
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Оставьте свой номер телефона, и мы свяжемся с вами в ближайшее
|
||||
время
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="+7 (999) 999-99-99"
|
||||
value={phone}
|
||||
onChange={handlePhoneChange}
|
||||
className="pl-10"
|
||||
required
|
||||
disabled={isLoading || isSubmitted}
|
||||
/>
|
||||
{error && (
|
||||
<div className="flex items-center gap-1 mt-1 text-red-500 text-sm">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-700 hover:bg-blue-800 text-white"
|
||||
disabled={isLoading || isSubmitted}
|
||||
>
|
||||
{isSubmitted ? (
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex items-center justify-center gap-2"
|
||||
>
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
<span>Заявка отправлена</span>
|
||||
</motion.span>
|
||||
) : isLoading ? (
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex items-center justify-center gap-2"
|
||||
>
|
||||
<span className="animate-spin rounded-full h-5 w-5 border-b-2 border-white" />
|
||||
<span>Отправка...</span>
|
||||
</motion.span>
|
||||
) : (
|
||||
'Отправить заявку'
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
Нажимая кнопку, вы соглашаетесь с{' '}
|
||||
<a
|
||||
href="/privacy-policy"
|
||||
className="text-blue-700 hover:text-blue-800"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
политикой конфиденциальности
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactModal;
|
280
app/components/Contacts.tsx
Normal file
280
app/components/Contacts.tsx
Normal file
@ -0,0 +1,280 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import {
|
||||
Phone,
|
||||
Mail,
|
||||
Clock,
|
||||
MapPin,
|
||||
Building,
|
||||
MessagesSquare,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const cityContacts = {
|
||||
Москва: {
|
||||
address: 'г. Москва, ул. Пресненская, д. 6, стр. 2',
|
||||
phone: '+7 (916) 830-58-58',
|
||||
email: 'ckeproekt@yandex.ru',
|
||||
workHours: 'ПН-ПТ: 8:00 - 20:00',
|
||||
mapLink: 'https://yandex.ru/maps/-/CCUzYXu7xC',
|
||||
coordinates: [37.539042, 55.74733],
|
||||
},
|
||||
Чебоксары: {
|
||||
address: 'г. Чебоксары, пр. Тракторостроителей, д. 11',
|
||||
phone: '+7 (916) 830-58-58',
|
||||
email: 'ckeproekt@yandex.ru',
|
||||
workHours: 'ПН-ПТ: 8:00 - 20:00',
|
||||
mapLink: 'https://yandex.ru/maps/-/CCUzYXBpkD',
|
||||
coordinates: [47.290091, 56.107257],
|
||||
},
|
||||
} as const;
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: MessagesSquare,
|
||||
title: 'Оперативная связь',
|
||||
description: 'Быстро отвечаем на звонки и сообщения в рабочее время',
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
title: 'Документы онлайн',
|
||||
description: 'Возможность получить документы в электронном виде',
|
||||
},
|
||||
{
|
||||
icon: Building,
|
||||
title: 'Удобное расположение',
|
||||
description: 'Офис в центре города с удобной транспортной доступностью',
|
||||
},
|
||||
];
|
||||
|
||||
interface ContactsProps {
|
||||
selectedCity: keyof typeof cityContacts;
|
||||
}
|
||||
|
||||
const Contacts = ({ selectedCity }: ContactsProps) => {
|
||||
const cityData = cityContacts[selectedCity];
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.2,
|
||||
delayChildren: 0.3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mapVariants = {
|
||||
hidden: { scale: 0.8, opacity: 0 },
|
||||
visible: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.8,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-20 bg-gray-50" id="contacts">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="max-w-3xl mx-auto text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
||||
Контакты
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Выберите удобный способ связи или посетите наш офис
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<motion.div variants={itemVariants} className="space-y-8">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
className="bg-white rounded-xl p-8 shadow-lg"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<motion.div
|
||||
className="flex items-start gap-4"
|
||||
whileHover={{ x: 10 }}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<motion.div
|
||||
whileHover={{ rotate: 360 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center"
|
||||
>
|
||||
<MapPin className="h-6 w-6 text-blue-700" />
|
||||
</motion.div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1">
|
||||
Адрес
|
||||
</h3>
|
||||
<p className="text-gray-600">{cityData.address}</p>
|
||||
<motion.a
|
||||
href={cityData.mapLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-700 hover:text-blue-800 font-medium inline-flex items-center gap-1 mt-2"
|
||||
whileHover={{ x: 5 }}
|
||||
>
|
||||
Открыть на карте
|
||||
</motion.a>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex items-start gap-4"
|
||||
whileHover={{ x: 10 }}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<motion.div
|
||||
whileHover={{ rotate: 360 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center"
|
||||
>
|
||||
<Phone className="h-6 w-6 text-blue-700" />
|
||||
</motion.div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1">
|
||||
Телефон
|
||||
</h3>
|
||||
<motion.a
|
||||
href={`tel:${cityData.phone}`}
|
||||
className="text-gray-600 hover:text-blue-700"
|
||||
whileHover={{ x: 5 }}
|
||||
>
|
||||
{cityData.phone}
|
||||
</motion.a>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex items-start gap-4"
|
||||
whileHover={{ x: 10 }}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<motion.div
|
||||
whileHover={{ rotate: 360 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center"
|
||||
>
|
||||
<Mail className="h-6 w-6 text-blue-700" />
|
||||
</motion.div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1">
|
||||
Email
|
||||
</h3>
|
||||
<motion.a
|
||||
href={`mailto:${cityData.email}`}
|
||||
className="text-gray-600 hover:text-blue-700"
|
||||
whileHover={{ x: 5 }}
|
||||
>
|
||||
{cityData.email}
|
||||
</motion.a>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex items-start gap-4"
|
||||
whileHover={{ x: 10 }}
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<motion.div
|
||||
whileHover={{ rotate: 360 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center"
|
||||
>
|
||||
<Clock className="h-6 w-6 text-blue-700" />
|
||||
</motion.div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1">
|
||||
Время работы
|
||||
</h3>
|
||||
<p className="text-gray-600">{cityData.workHours}</p>
|
||||
<p className="text-gray-600">Сб-Вс: выходной</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={itemVariants}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="bg-white rounded-xl p-6 shadow-lg text-center"
|
||||
>
|
||||
<motion.div
|
||||
className="mb-4 flex justify-center"
|
||||
whileHover={{ rotate: 360 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<feature.icon className="h-6 w-6 text-blue-700" />
|
||||
</div>
|
||||
</motion.div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm">
|
||||
{feature.description}
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={mapVariants}
|
||||
className="bg-white rounded-xl shadow-lg overflow-hidden h-[600px]"
|
||||
>
|
||||
<iframe
|
||||
src={`https://yandex.ru/map-widget/v1/?ll=${cityData.coordinates[0]},${cityData.coordinates[1]}&z=16&mode=search&whatshere[point]=${cityData.coordinates[0]},${cityData.coordinates[1]}&whatshere[zoom]=16`}
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameBorder="0"
|
||||
title="Карта с местоположением офиса"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contacts;
|
77
app/components/DocumentLayout.tsx
Normal file
77
app/components/DocumentLayout.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface DocumentLayoutProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DocumentLayout = ({ title, children }: DocumentLayoutProps) => {
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={containerVariants}
|
||||
className="max-w-4xl mx-auto"
|
||||
>
|
||||
{/* Навигация */}
|
||||
<motion.div variants={itemVariants} className="mb-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center text-blue-700 hover:text-blue-800"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
<span>Вернуться на главную</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Заголовок */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="bg-white rounded-xl p-8 shadow-lg mb-8"
|
||||
>
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-900">
|
||||
{title}
|
||||
</h1>
|
||||
</motion.div>
|
||||
|
||||
{/* Контент */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="bg-white rounded-xl p-8 shadow-lg prose prose-blue max-w-none"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentLayout;
|
206
app/components/Footer.tsx
Normal file
206
app/components/Footer.tsx
Normal file
@ -0,0 +1,206 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import { Building, MapPin, Phone, Mail } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'О компании', href: '#about' },
|
||||
{ name: 'Преимущества', href: '#why-us' },
|
||||
{ name: 'Как мы работаем', href: '#workflow' },
|
||||
{ name: 'Сертификаты', href: '#certificates' },
|
||||
{ name: 'Услуги', href: '#services' },
|
||||
{ name: 'Контакты', href: '#contacts' },
|
||||
];
|
||||
|
||||
const legalLinks = [
|
||||
{ name: 'Политика конфиденциальности', href: '/privacy-policy' },
|
||||
{ name: 'Пользовательское соглашение', href: '/terms' },
|
||||
{ name: 'Обработка персональных данных', href: '/personal-data' },
|
||||
];
|
||||
|
||||
const cityContacts = {
|
||||
Москва: {
|
||||
address: 'г. Москва, ул. Пресненская, д. 6, стр. 2',
|
||||
phone: '+7 (916) 830-58-58',
|
||||
email: 'ckeproekt@yandex.ru',
|
||||
},
|
||||
Чебоксары: {
|
||||
address: 'г. Чебоксары, пр. Тракторостроителей, д. 11',
|
||||
phone: '+7 (916) 830-58-58',
|
||||
email: 'ckeproekt@yandex.ru',
|
||||
},
|
||||
};
|
||||
|
||||
interface FooterProps {
|
||||
selectedCity: keyof typeof cityContacts;
|
||||
}
|
||||
|
||||
const Footer = ({ selectedCity }: FooterProps) => {
|
||||
const cityData = cityContacts[selectedCity];
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<footer ref={ref} className="bg-gray-50 border-t">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
variants={containerVariants}
|
||||
className="py-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8"
|
||||
>
|
||||
{/* Колонка 1: О компании */}
|
||||
<motion.div variants={itemVariants} className="space-y-4">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="flex items-center space-x-2 text-blue-700"
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center space-x-2 text-blue-700"
|
||||
>
|
||||
<Building className="h-6 w-6" />
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">ЦКЕ</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Центр комплексных экспертиз
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Профессиональная экспертиза и обследование объектов с гарантией
|
||||
качества
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Колонка 2: Навигация */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Навигация</h3>
|
||||
<nav className="grid grid-cols-1 gap-2">
|
||||
{navigation.map((item) => (
|
||||
<motion.div key={item.href} whileHover={{ x: 5 }}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="text-gray-600 hover:text-blue-700 transition-colors text-sm"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</nav>
|
||||
</motion.div>
|
||||
|
||||
{/* Колонка 3: Контакты */}
|
||||
<motion.div variants={itemVariants} className="space-y-4">
|
||||
<h3 className="font-semibold text-gray-900">Контакты</h3>
|
||||
<div className="space-y-3">
|
||||
<motion.div
|
||||
whileHover={{ x: 5 }}
|
||||
className="flex items-center gap-2 text-sm text-gray-600"
|
||||
>
|
||||
<MapPin className="h-4 w-4 text-blue-700 flex-shrink-0" />
|
||||
<span>{cityData.address}</span>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
whileHover={{ x: 5 }}
|
||||
className="flex items-center gap-2 text-sm text-gray-600"
|
||||
>
|
||||
<Phone className="h-4 w-4 text-blue-700 flex-shrink-0" />
|
||||
<a
|
||||
href={`tel:${cityData.phone}`}
|
||||
className="hover:text-blue-700"
|
||||
>
|
||||
{cityData.phone}
|
||||
</a>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
whileHover={{ x: 5 }}
|
||||
className="flex items-center gap-2 text-sm text-gray-600"
|
||||
>
|
||||
<Mail className="h-4 w-4 text-blue-700 flex-shrink-0" />
|
||||
<a
|
||||
href={`mailto:${cityData.email}`}
|
||||
className="hover:text-blue-700"
|
||||
>
|
||||
{cityData.email}
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Колонка 4: Документы */}
|
||||
<motion.div variants={itemVariants}>
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Документы</h3>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{legalLinks.map((link) => (
|
||||
<motion.div key={link.href} whileHover={{ x: 5 }}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-gray-600 hover:text-blue-700 transition-colors"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Нижняя часть футера */}
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
className="border-t border-gray-200 py-6"
|
||||
>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="flex flex-col md:flex-row justify-between items-center gap-4 text-sm text-gray-500"
|
||||
>
|
||||
<div>
|
||||
© {new Date().getFullYear()} ЦКЕ - Центр комплексных экспертиз.
|
||||
Все права защищены
|
||||
</div>
|
||||
<motion.div whileHover={{ scale: 1.05 }}>
|
||||
Разработка сайта{' '}
|
||||
<a
|
||||
href="https://biveki.ru"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-700 hover:text-blue-800 font-medium"
|
||||
>
|
||||
Biveki Group
|
||||
</a>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
222
app/components/Header.tsx
Normal file
222
app/components/Header.tsx
Normal file
@ -0,0 +1,222 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { MapPin, Phone, Building, Menu, X } from 'lucide-react';
|
||||
import ContactModal from './ContactModal';
|
||||
|
||||
const cityData = {
|
||||
Москва: {
|
||||
phone: '+7 (916) 830-58-58',
|
||||
},
|
||||
Чебоксары: {
|
||||
phone: '+7 (916) 830-58-58',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const navigation = [
|
||||
{ name: 'О компании', href: '#about' },
|
||||
{ name: 'Преимущества', href: '#why-us' },
|
||||
{ name: 'Как мы работаем', href: '#workflow' },
|
||||
{ name: 'Сертификаты', href: '#certificates' },
|
||||
{ name: 'Услуги', href: '#services' },
|
||||
{ name: 'Контакты', href: '#contacts' },
|
||||
];
|
||||
|
||||
type CityKey = keyof typeof cityData;
|
||||
|
||||
interface HeaderProps {
|
||||
selectedCity: CityKey;
|
||||
onCityChange: (city: CityKey) => void;
|
||||
}
|
||||
|
||||
const Header = ({ selectedCity, onCityChange }: HeaderProps) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [lastScrollY, setLastScrollY] = useState(0);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const currentScrollY = window.scrollY;
|
||||
setIsVisible(currentScrollY < lastScrollY || currentScrollY < 100);
|
||||
setLastScrollY(currentScrollY);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [lastScrollY]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.header
|
||||
initial={{ y: -100, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
|
||||
className={`fixed top-0 left-0 right-0 w-full border-b bg-white/95 backdrop-blur-sm z-50 shadow-sm transition-all duration-300 ${
|
||||
isVisible ? 'translate-y-0' : '-translate-y-full'
|
||||
}`}
|
||||
>
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center space-x-2 text-blue-700"
|
||||
>
|
||||
<Building className="h-6 w-6" />
|
||||
<div>
|
||||
<span className="text-xl font-bold">ЦКЕ</span>
|
||||
<span className="block text-sm font-normal text-gray-600">
|
||||
Центр комплексных экспертиз
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
className="lg:hidden p-2 hover:bg-gray-100 rounded-lg"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
>
|
||||
{isMenuOpen ? (
|
||||
<X className="h-6 w-6 text-gray-600" />
|
||||
) : (
|
||||
<Menu className="h-6 w-6 text-gray-600" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<nav className="hidden lg:flex items-center space-x-6">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-gray-600 hover:text-blue-700 transition-colors text-sm"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="hidden lg:flex items-center space-x-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex items-center space-x-2 hover:bg-gray-100"
|
||||
>
|
||||
<MapPin className="h-4 w-4 text-blue-700" />
|
||||
<span>{selectedCity}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onCityChange('Москва')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Москва
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onCityChange('Чебоксары')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Чебоксары
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<a
|
||||
href={`tel:${cityData[selectedCity].phone}`}
|
||||
className="flex items-center space-x-2 text-gray-600 hover:text-blue-700"
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
<span>{cityData[selectedCity].phone}</span>
|
||||
</a>
|
||||
|
||||
<Button
|
||||
className="bg-blue-700 hover:bg-blue-800 text-white"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
Оставить заявку
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`lg:hidden overflow-hidden transition-all duration-300 ${
|
||||
isMenuOpen ? 'max-h-[500px] mt-4' : 'max-h-0'
|
||||
}`}
|
||||
>
|
||||
<nav className="flex flex-col space-y-4 pb-4">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="text-gray-600 hover:text-blue-700 transition-colors text-sm py-2"
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
<div className="pt-4 border-t">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="w-full justify-start">
|
||||
<MapPin className="h-4 w-4 text-blue-700 mr-2" />
|
||||
<span>{selectedCity}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onCityChange('Москва')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Москва
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onCityChange('Чебоксары')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Чебоксары
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<a
|
||||
href={`tel:${cityData[selectedCity].phone}`}
|
||||
className="flex items-center space-x-2 text-gray-600 hover:text-blue-700 py-2"
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
<span>{cityData[selectedCity].phone}</span>
|
||||
</a>
|
||||
|
||||
<Button
|
||||
className="w-full bg-blue-700 hover:bg-blue-800 text-white mt-4"
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
Оставить заявку
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</motion.header>
|
||||
<div className="h-[72px]" />
|
||||
|
||||
<ContactModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
208
app/components/Hero.tsx
Normal file
208
app/components/Hero.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Phone, CheckCircle2, AlertCircle } from 'lucide-react';
|
||||
import { formatPhoneNumber, validatePhoneNumber } from '@/lib/utils';
|
||||
import { sendTelegramNotification } from '@/lib/telegram';
|
||||
import ContactModal from './ContactModal';
|
||||
|
||||
const Hero = () => {
|
||||
const [phone, setPhone] = useState('');
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const formattedPhone = formatPhoneNumber(e.target.value);
|
||||
setPhone(formattedPhone);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validatePhoneNumber(phone)) {
|
||||
setError('Пожалуйста, введите корректный российский номер телефона');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const success = await sendTelegramNotification(phone, 'Главный экран');
|
||||
|
||||
if (success) {
|
||||
setIsSubmitted(true);
|
||||
setError('');
|
||||
setTimeout(() => {
|
||||
setIsSubmitted(false);
|
||||
setPhone('');
|
||||
}, 3000);
|
||||
} else {
|
||||
setError(
|
||||
'Произошла ошибка при отправке заявки. Пожалуйста, попробуйте позже.'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(
|
||||
'Произошла ошибка при отправке заявки. Пожалуйста, попробуйте позже.'
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.8,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative bg-gradient-to-r from-blue-600 to-blue-800">
|
||||
{/* Фоновый паттерн */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(\"data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E\")",
|
||||
backgroundSize: '30px 30px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="container mx-auto px-4 py-16 sm:py-20 relative"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<div className="max-w-3xl mx-auto text-center text-white space-y-8">
|
||||
{/* Заголовок */}
|
||||
<motion.h1
|
||||
className="text-3xl sm:text-4xl md:text-5xl font-bold leading-tight"
|
||||
variants={itemVariants}
|
||||
>
|
||||
Независимая строительно-техническая экспертиза в Москве и Чебоксарах
|
||||
</motion.h1>
|
||||
|
||||
{/* Описание */}
|
||||
<motion.p
|
||||
className="text-lg sm:text-xl md:text-2xl text-blue-100"
|
||||
variants={itemVariants}
|
||||
>
|
||||
Проводим профессиональную экспертизу при заливах, обследование
|
||||
инженерных систем, оценку качества строительных работ и
|
||||
тепловизионное обследование. Гарантируем точность заключений и
|
||||
юридическую поддержку.
|
||||
</motion.p>
|
||||
|
||||
{/* Форма */}
|
||||
<motion.div
|
||||
className="max-w-md mx-auto bg-white/10 backdrop-blur-sm rounded-lg p-6 space-y-4"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<h3 className="text-lg font-medium">
|
||||
Получите бесплатную консультацию эксперта
|
||||
</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-blue-200" />
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="+7 (999) 999-99-99"
|
||||
value={phone}
|
||||
onChange={handlePhoneChange}
|
||||
className="pl-10 bg-white/10 border-white/20 text-white placeholder:text-blue-200 focus:border-white"
|
||||
required
|
||||
disabled={isLoading || isSubmitted}
|
||||
/>
|
||||
{error && (
|
||||
<div className="flex items-center gap-1 mt-1 text-red-300 text-sm">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-white text-blue-700 hover:bg-blue-50"
|
||||
disabled={isLoading || isSubmitted}
|
||||
>
|
||||
{isSubmitted ? (
|
||||
<span className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="h-5 w-5" />
|
||||
<span>Заявка отправлена</span>
|
||||
</span>
|
||||
) : isLoading ? (
|
||||
<span className="flex items-center space-x-2">
|
||||
<span className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-700" />
|
||||
<span>Отправка...</span>
|
||||
</span>
|
||||
) : (
|
||||
'Получить консультацию'
|
||||
)}
|
||||
</Button>
|
||||
<p className="text-sm text-blue-200">
|
||||
Нажимая кнопку, вы соглашаетесь с{' '}
|
||||
<a
|
||||
href="/privacy-policy"
|
||||
className="text-white hover:text-blue-100 underline"
|
||||
>
|
||||
политикой конфиденциальности
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
</motion.div>
|
||||
|
||||
{/* Преимущества */}
|
||||
<motion.div
|
||||
className="grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-6 mt-12"
|
||||
variants={itemVariants}
|
||||
>
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-4">
|
||||
<p className="font-bold text-xl sm:text-2xl mb-2">9+ лет</p>
|
||||
<p className="text-blue-100">опыта в экспертизе</p>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-4">
|
||||
<p className="font-bold text-xl sm:text-2xl mb-2">500+</p>
|
||||
<p className="text-blue-100">выполненных проектов</p>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-4">
|
||||
<p className="font-bold text-xl sm:text-2xl mb-2">100%</p>
|
||||
<p className="text-blue-100">гарантия качества</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<ContactModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
33
app/components/Loader.tsx
Normal file
33
app/components/Loader.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Building } from 'lucide-react';
|
||||
|
||||
const Loader = () => {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="fixed inset-0 bg-white z-[100] flex items-center justify-center"
|
||||
>
|
||||
<div className="text-center">
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="flex items-center justify-center mb-4"
|
||||
>
|
||||
<Building className="h-12 w-12 text-blue-700 animate-bounce" />
|
||||
</motion.div>
|
||||
<div className="relative w-48 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="absolute inset-y-0 left-0 bg-blue-700 rounded-full animate-loading-bar" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
236
app/components/Services.tsx
Normal file
236
app/components/Services.tsx
Normal file
@ -0,0 +1,236 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import {
|
||||
Home,
|
||||
Droplets,
|
||||
Waves,
|
||||
CheckSquare,
|
||||
Thermometer,
|
||||
Building,
|
||||
FlaskConical,
|
||||
ArrowRight,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import ContactModal from './ContactModal';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const services = [
|
||||
{
|
||||
icon: Droplets,
|
||||
title: 'Экспертиза при заливе',
|
||||
slug: 'flood-expertise',
|
||||
description:
|
||||
'Строительно-техническая экспертиза для определения причины залития помещений, оценка ущерба и рекомендации по устранению последствий.',
|
||||
details: [
|
||||
'Определение источника протечки',
|
||||
'Оценка нанесенного ущерба',
|
||||
'Расчет стоимости восстановительных работ',
|
||||
'Составление экспертного заключения',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Waves,
|
||||
title: 'Обследование канализации',
|
||||
slug: 'sewerage-inspection',
|
||||
description:
|
||||
'Профессиональное обследование канализационных систем с использованием современного диагностического оборудования.',
|
||||
details: [
|
||||
'Видеодиагностика труб',
|
||||
'Проверка герметичности',
|
||||
'Оценка состояния коммуникаций',
|
||||
'Рекомендации по ремонту',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Home,
|
||||
title: 'Признание дома жилым',
|
||||
slug: 'house-recognition',
|
||||
description:
|
||||
'Экспертиза для признания дома пригодным для круглогодичного проживания, оценка соответствия всем необходимым нормам.',
|
||||
details: [
|
||||
'Оценка конструкций',
|
||||
'Проверка инженерных систем',
|
||||
'Анализ микроклимата',
|
||||
'Подготовка документации',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: CheckSquare,
|
||||
title: 'Экспертиза ремонтных работ',
|
||||
slug: 'renovation-expertise',
|
||||
description:
|
||||
'Определение качества выполненных ремонтных работ по отделке помещений, выявление дефектов и нарушений.',
|
||||
details: [
|
||||
'Проверка качества материалов',
|
||||
'Оценка технологии работ',
|
||||
'Выявление дефектов',
|
||||
'Рекомендации по устранению',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Thermometer,
|
||||
title: 'Тепловизионная экспертиза',
|
||||
slug: 'thermal-inspection',
|
||||
description:
|
||||
'Определение утечек тепла с помощью современного тепловизионного оборудования, выявление проблемных зон.',
|
||||
details: [
|
||||
'Тепловизионная съемка',
|
||||
'Анализ теплопотерь',
|
||||
'Выявление мостиков холода',
|
||||
'Рекомендации по утеплению',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: Building,
|
||||
title: 'Контроль строительных работ',
|
||||
slug: 'construction-control',
|
||||
description:
|
||||
'Определение качества строительно-монтажных работ на всех этапах строительства.',
|
||||
details: [
|
||||
'Проверка соответствия проекту',
|
||||
'Контроль технологий',
|
||||
'Оценка материалов',
|
||||
'Выявление нарушений',
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: FlaskConical,
|
||||
title: 'Обмер помещений',
|
||||
slug: 'room-measurement',
|
||||
description:
|
||||
'Профессиональные обмеры помещений с использованием современного оборудования и составлением точных планов.',
|
||||
details: [
|
||||
'Замеры всех помещений',
|
||||
'Создание поэтажных планов',
|
||||
'Расчет площадей',
|
||||
'Составление технического паспорта',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const Services = () => {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
duration: 0.3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-20 bg-white" id="services">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="max-w-3xl mx-auto text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
||||
Наши услуги
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Предоставляем полный спектр услуг по экспертизе и обследованию
|
||||
объектов
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{services.map((service, index) => (
|
||||
<Link href={`/services/${service.slug}`} key={index}>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
transition: { duration: 0.2 },
|
||||
}}
|
||||
className="group bg-gray-50 rounded-xl p-6 hover:bg-blue-700 transition-all duration-300 h-full"
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="flex-shrink-0">
|
||||
<motion.div
|
||||
whileHover={{ rotate: 360 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="w-12 h-12 rounded-lg bg-blue-100 group-hover:bg-blue-600 flex items-center justify-center transition-colors"
|
||||
>
|
||||
<service.icon className="h-6 w-6 text-blue-700 group-hover:text-white" />
|
||||
</motion.div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 group-hover:text-white mb-2">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 group-hover:text-blue-100 mb-4">
|
||||
{service.description}
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{service.details.map((detail, idx) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="flex items-center text-gray-600 group-hover:text-blue-100"
|
||||
>
|
||||
<ArrowRight className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||
<span>{detail}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div variants={itemVariants} className="mt-16 text-center">
|
||||
<p className="text-gray-600 mb-6 text-lg">
|
||||
Подберем оптимальное решение под ваши задачи.
|
||||
<br />
|
||||
Все консультации бесплатны.
|
||||
</p>
|
||||
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||
<Button
|
||||
className="bg-blue-700 hover:bg-blue-800 text-white px-8"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
Получить консультацию
|
||||
</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<ContactModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Services;
|
175
app/components/WhyUs.tsx
Normal file
175
app/components/WhyUs.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import {
|
||||
Shield,
|
||||
Scale,
|
||||
PiggyBank,
|
||||
Building2,
|
||||
Umbrella,
|
||||
FileCheck,
|
||||
Car,
|
||||
ThumbsUp,
|
||||
Gavel,
|
||||
} from 'lucide-react';
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Shield,
|
||||
title: 'Гарантия качества',
|
||||
description:
|
||||
'Имеем все необходимые гарантии качества работ. Некачественная экспертиза, приводящая к ущербу наказуема, поэтому мы обязаны работать качественно.',
|
||||
},
|
||||
{
|
||||
icon: PiggyBank,
|
||||
title: 'Лучшие цены',
|
||||
description:
|
||||
'Предлагаем конкурентные цены на рынке при неизменно высоком качестве услуг.',
|
||||
},
|
||||
{
|
||||
icon: Building2,
|
||||
title: 'Членство в СРО',
|
||||
description:
|
||||
'Состоим в профильных СРО, предоставляющей разрешение на работы и страхование до 30 млн рублей.',
|
||||
},
|
||||
{
|
||||
icon: Umbrella,
|
||||
title: 'Страховая защита',
|
||||
description:
|
||||
'Застрахованы в Британском страховом доме на 10 млн рублей, что гарантирует безопасность наших клиентов.',
|
||||
},
|
||||
{
|
||||
icon: FileCheck,
|
||||
title: 'Полный пакет документов',
|
||||
description:
|
||||
'Имеем все необходимые квалификационные и страховые документы для проведения экспертиз любой сложности.',
|
||||
},
|
||||
{
|
||||
icon: Car,
|
||||
title: 'Оперативный выезд',
|
||||
description:
|
||||
'Обеспечиваем быстрый выезд специалистов на объект для проведения необходимых исследований.',
|
||||
},
|
||||
{
|
||||
icon: ThumbsUp,
|
||||
title: 'Ответственный подход',
|
||||
description:
|
||||
'Не беремся за дело если не уверены в результате. Ценим свою репутацию и время клиентов.',
|
||||
},
|
||||
{
|
||||
icon: Gavel,
|
||||
title: 'Юридическая поддержка',
|
||||
description:
|
||||
'Предоставляем полное юридическое сопровождение на всех этапах работы.',
|
||||
},
|
||||
];
|
||||
|
||||
const WhyUs = () => {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
delayChildren: 0.3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const iconVariants = {
|
||||
hidden: { scale: 0, rotate: -180 },
|
||||
visible: {
|
||||
scale: 1,
|
||||
rotate: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 200,
|
||||
damping: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-20 bg-white" id="why-us">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="max-w-3xl mx-auto text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
||||
Почему выбирают нас?
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Мы гарантируем качество, надежность и профессионализм в каждом
|
||||
проекте
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={itemVariants}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
transition: { duration: 0.2 },
|
||||
}}
|
||||
className="bg-gray-50 rounded-xl p-6 hover:shadow-lg transition-shadow duration-300"
|
||||
>
|
||||
<div className="flex items-start space-x-4">
|
||||
<motion.div className="flex-shrink-0" variants={iconVariants}>
|
||||
<div className="w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<feature.icon className="h-6 w-6 text-blue-700" />
|
||||
</div>
|
||||
</motion.div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div variants={itemVariants} className="mt-16 text-center">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="inline-flex items-center space-x-2 bg-blue-700 text-white px-6 py-3 rounded-lg cursor-pointer"
|
||||
>
|
||||
<Scale className="h-5 w-5" />
|
||||
<span className="font-medium">
|
||||
Ваша безопасность - наш главный приоритет
|
||||
</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhyUs;
|
189
app/components/WorkFlow.tsx
Normal file
189
app/components/WorkFlow.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { motion, useInView } from 'framer-motion';
|
||||
import {
|
||||
PhoneCall,
|
||||
MessageSquare,
|
||||
FileSignature,
|
||||
Search,
|
||||
FileText,
|
||||
Scale,
|
||||
} from 'lucide-react';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
number: '01',
|
||||
icon: PhoneCall,
|
||||
title: 'Заявка или звонок',
|
||||
description:
|
||||
'Свяжитесь с нами удобным для вас способом — мы всегда на связи и готовы помочь',
|
||||
},
|
||||
{
|
||||
number: '02',
|
||||
icon: MessageSquare,
|
||||
title: 'Первичная консультация',
|
||||
description:
|
||||
'Мы детально изучаем ваш запрос и предлагаем наилучшие решения для вашего комфорта!',
|
||||
},
|
||||
{
|
||||
number: '03',
|
||||
icon: FileSignature,
|
||||
title: 'Подписание договора',
|
||||
description:
|
||||
'Мы предоставляем чёткие гарантии, учитываем все детали и внимательно разъясняем каждое предложение, чтобы вы всегда были уверены и довольны!',
|
||||
},
|
||||
{
|
||||
number: '04',
|
||||
icon: Search,
|
||||
title: 'Анализ данных',
|
||||
description: 'Выезд на объект проведение независимой экспертизы',
|
||||
},
|
||||
{
|
||||
number: '05',
|
||||
icon: FileText,
|
||||
title: 'Камеральная обработка данных',
|
||||
description: 'Составление и передача результатов экспертизы',
|
||||
},
|
||||
{
|
||||
number: '06',
|
||||
icon: Scale,
|
||||
title: 'Юридическая поддержка',
|
||||
description:
|
||||
'Если вам понадобится юридическая поддержка, наши опытные юристы будут рядом, готовые обеспечить вам квалифицированное сопровождение на каждом этапе!',
|
||||
},
|
||||
];
|
||||
|
||||
const WorkFlow = () => {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px' });
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.2,
|
||||
delayChildren: 0.3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { y: 20, opacity: 0 },
|
||||
visible: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const numberVariants = {
|
||||
hidden: { scale: 0, rotate: -180 },
|
||||
visible: {
|
||||
scale: 1,
|
||||
rotate: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 200,
|
||||
damping: 15,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const lineVariants = {
|
||||
hidden: { width: 0 },
|
||||
visible: {
|
||||
width: '100%',
|
||||
transition: {
|
||||
duration: 0.8,
|
||||
ease: 'easeInOut',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-20 bg-gray-50" id="workflow">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="max-w-3xl mx-auto text-center mb-16"
|
||||
>
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
||||
Порядок работы
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
Прозрачный и эффективный процесс работы с каждым клиентом
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{steps.map((step, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
variants={itemVariants}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
transition: { duration: 0.2 },
|
||||
}}
|
||||
className="relative bg-white rounded-xl p-6 shadow-lg hover:shadow-xl transition-shadow"
|
||||
>
|
||||
<motion.div
|
||||
variants={numberVariants}
|
||||
className="absolute -top-4 -left-4 w-12 h-12 bg-blue-700 rounded-lg flex items-center justify-center text-white font-bold"
|
||||
>
|
||||
{step.number}
|
||||
</motion.div>
|
||||
|
||||
<div className="pt-4">
|
||||
<motion.div
|
||||
className="mb-6 flex justify-center"
|
||||
whileHover={{ rotate: 360 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="w-16 h-16 rounded-full bg-blue-50 flex items-center justify-center">
|
||||
<step.icon className="h-8 w-8 text-blue-700" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-3">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-gray-600">{step.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="mt-16 max-w-2xl mx-auto text-center"
|
||||
>
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.02 }}
|
||||
className="bg-blue-50 rounded-lg p-6"
|
||||
>
|
||||
<p className="text-blue-700 font-medium">
|
||||
На каждом этапе мы обеспечиваем полную прозрачность процесса и
|
||||
держим вас в курсе всех действий. Наша цель - сделать
|
||||
сотрудничество максимально комфортным и эффективным для вас.
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkFlow;
|
Reference in New Issue
Block a user