Initial commit: YourHouse project
This commit is contained in:
41
src/app/api/send-telegram/route.ts
Normal file
41
src/app/api/send-telegram/route.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { type, name, phone, material, area, finish, finance, message } = await req.json();
|
||||
|
||||
const token = '7422874036:AAE54kNXkKv2y4iwBmEuexSefuh9fs2v2dc';
|
||||
const chatId = '965454906'; // chat_id пользователя
|
||||
|
||||
let text = '';
|
||||
|
||||
if (type === 'contact') {
|
||||
text = `🚀 Новая заявка "Готовы начать строительство?"\n\n🎯 <b>Ваш Дом - Контактная форма</b>\n\n👤 <b>Имя:</b> ${name}\n📞 <b>Телефон:</b> ${phone}\n🏠 <b>Тип заявки:</b> Готов начать строительство\n⏰ <b>Время:</b> ${new Date().toLocaleString('ru-RU')}\n\n✅ <i>Клиент готов обсуждать строительство дома!</i>`;
|
||||
} else if (material === 'Контакты') {
|
||||
text = `📩 Новое сообщение с сайта ВашДом:\n\n👤 Имя: ${name}\n📞 Телефон: ${phone}\n💬 Сообщение: ${message || '-'}\n`;
|
||||
} else if (material === 'Экскурсия') {
|
||||
text = `🚌 Заявка на экскурсию с сайта ВашДом:\n\n👤 Имя: ${name}\n📞 Телефон: ${phone}`;
|
||||
} else if (material === 'Звонок') {
|
||||
text = `📞 Заявка на звонок с сайта ВашДом:\n\n👤 Имя: ${name}\n📞 Телефон: ${phone}\n💬 Сообщение: ${message || '-'}`;
|
||||
} else if (material === 'Расчет стоимости') {
|
||||
text = `💰 Новая заявка на расчет стоимости дома!\n\n🎯 <b>Ваш Дом - Заявка с главной страницы</b>\n\n👤 <b>Имя:</b> ${name}\n📞 <b>Телефон:</b> ${phone}\n🏠 <b>Тип заявки:</b> Расчет стоимости дома\n⏰ <b>Время:</b> ${new Date().toLocaleString('ru-RU')}\n\n✅ <i>Клиент заинтересован в строительстве дома и готов обсудить проект!</i>`;
|
||||
} else {
|
||||
text = `🏠 Новая заявка на расчет стоимости с сайта ВашДом:\n\n👤 Имя: ${name}\n📞 Телефон: ${phone}\n🧱 Материал: ${material}\n📏 Площадь: ${area}\n✨ Отделка: ${finish}\n💰 Финансирование: ${finance}`;
|
||||
}
|
||||
|
||||
const url = `https://api.telegram.org/bot${token}/sendMessage`;
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: 'HTML',
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
return NextResponse.json({ ok: true });
|
||||
} else {
|
||||
return NextResponse.json({ ok: false }, { status: 500 });
|
||||
}
|
||||
}
|
BIN
src/app/favicon.ico
Normal file
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
151
src/app/globals.css
Normal file
151
src/app/globals.css
Normal file
@ -0,0 +1,151 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: rgb(var(--background-rgb));
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Стили для скроллбара */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Анимации */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Дополнительные анимации для блока "О нас" */
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 30px rgba(59, 130, 246, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInFromLeft {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInFromRight {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(50px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-glow {
|
||||
animation: glow 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.animate-slide-in-left {
|
||||
animation: slideInFromLeft 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-right {
|
||||
animation: slideInFromRight 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scaleIn 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-pulse-glow {
|
||||
animation: pulse-glow 3s ease-in-out infinite;
|
||||
}
|
34
src/app/layout.tsx
Normal file
34
src/app/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
24
src/app/not-found.tsx
Normal file
24
src/app/not-found.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import Link from 'next/link';
|
||||
import FadeInSection from '@/components/FadeInSection';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-blue-50 to-white px-4">
|
||||
<FadeInSection as="div" className="flex flex-col items-center text-center max-w-lg w-full">
|
||||
<div className="mb-8">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" className="mx-auto">
|
||||
<rect x="20" y="60" width="80" height="40" rx="8" fill="#2563eb"/>
|
||||
<polygon points="60,20 20,60 100,60" fill="#3b82f6"/>
|
||||
<rect x="50" y="80" width="20" height="20" rx="4" fill="#fff"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-6xl font-extrabold text-blue-700 mb-4">404</h1>
|
||||
<p className="text-2xl font-semibold text-gray-800 mb-2">Страница не найдена</p>
|
||||
<p className="text-gray-500 mb-8">Возможно, вы ошиблись адресом или страница была удалена.</p>
|
||||
<Link href="/" className="inline-block bg-blue-600 text-white px-8 py-3 rounded-full text-lg font-semibold shadow-lg hover:bg-blue-700 transition-colors">
|
||||
На главную
|
||||
</Link>
|
||||
</FadeInSection>
|
||||
</main>
|
||||
);
|
||||
}
|
227
src/app/page.tsx
Normal file
227
src/app/page.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from 'react';
|
||||
import Header from '@/components/Header';
|
||||
import AboutSection from '@/components/AboutSection';
|
||||
import ProjectsSection from '@/components/ProjectsSection';
|
||||
import WhyChooseUsSection from '@/components/WhyChooseUsSection';
|
||||
import TeamSection from '@/components/TeamSection';
|
||||
import ReviewsSection from '@/components/ReviewsSection';
|
||||
import ContactSection from '@/components/ContactSection';
|
||||
import Footer from '@/components/Footer';
|
||||
import Image from 'next/image';
|
||||
import HouseCalculatorModal from '@/components/HouseCalculatorModal';
|
||||
import CatalogRequestModal from '@/components/CatalogRequestModal';
|
||||
import ExcursionModal from '@/components/ExcursionModal';
|
||||
import FadeInSection from '@/components/FadeInSection';
|
||||
import Preloader from '@/components/Preloader';
|
||||
|
||||
export default function Home() {
|
||||
const [isCalculatorOpen, setIsCalculatorOpen] = useState(false);
|
||||
const [isCatalogModalOpen, setIsCatalogModalOpen] = useState(false);
|
||||
const [isExcursionModalOpen, setIsExcursionModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hidePreloader, setHidePreloader] = useState(false);
|
||||
|
||||
// Form states
|
||||
const [phone, setPhone] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setTimeout(() => setHidePreloader(true), 600); // для плавного исчезновения
|
||||
}, 1200);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const validatePhone = (value: string) => {
|
||||
const digits = value.replace(/\D/g, '');
|
||||
if (digits.length !== 11) return false;
|
||||
if (!(digits.startsWith('7') || digits.startsWith('8'))) return false;
|
||||
if (/^(7|8)0{10}$/.test(digits)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateName = (value: string) => {
|
||||
return /^[А-Яа-яA-Za-zЁё\s\-]{2,}$/.test(value.trim());
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
if (!validateName(name)) {
|
||||
setError('Пожалуйста, укажите корректное имя (только буквы, не менее 2 символов)');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validatePhone(phone)) {
|
||||
setError('Пожалуйста, укажите корректный российский номер телефона');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Отправляем данные в Telegram
|
||||
try {
|
||||
const res = await fetch('/api/send-telegram', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
phone,
|
||||
material: 'Расчет стоимости',
|
||||
message: 'Запрос расчета стоимости дома'
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// Если отправка успешна, открываем калькулятор
|
||||
setIsCalculatorOpen(true);
|
||||
} else {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
}
|
||||
} catch {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
{!hidePreloader && (
|
||||
<div className={`fixed inset-0 z-[9999] transition-opacity duration-500 ${isLoading ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}>
|
||||
<Preloader />
|
||||
</div>
|
||||
)}
|
||||
<Header />
|
||||
<HouseCalculatorModal
|
||||
isOpen={isCalculatorOpen}
|
||||
onClose={() => setIsCalculatorOpen(false)}
|
||||
userName={name}
|
||||
userPhone={phone}
|
||||
/>
|
||||
<CatalogRequestModal isOpen={isCatalogModalOpen} onClose={() => setIsCatalogModalOpen(false)} />
|
||||
<ExcursionModal isOpen={isExcursionModalOpen} onClose={() => setIsExcursionModalOpen(false)} />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative min-h-screen pt-20 pb-16 flex items-center">
|
||||
{/* Background Image */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/images/header.jpg"
|
||||
alt="Строительство домов"
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/60"></div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<div className="flex flex-col lg:flex-row items-center justify-between gap-12">
|
||||
{/* Left Content */}
|
||||
<FadeInSection as="div" className="flex-1 text-white" delay={0.2}>
|
||||
|
||||
<h1 className="text-4xl lg:text-6xl xl:text-7xl font-bold text-white mb-6 leading-tight">
|
||||
СТРОИТЕЛЬСТВО КАМЕННЫХ<br />
|
||||
И КАРКАСНЫХ ДОМОВ<br />
|
||||
С ФИКСАЦИЕЙ ЦЕНЫ
|
||||
</h1>
|
||||
|
||||
<p className="text-xl lg:text-2xl text-white/90 mb-12">
|
||||
Построим технологичный дом от 6 млн. руб за 90 дней
|
||||
</p>
|
||||
|
||||
{/* Stats Section */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-6 mb-8">
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 text-center border border-white/20 hover:bg-white/15 transition-all duration-300 shadow-lg">
|
||||
<div className="text-3xl lg:text-4xl font-bold text-white mb-2">+100</div>
|
||||
<div className="text-white/80 text-sm font-medium">Реализованных объектов</div>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 text-center border border-white/20 hover:bg-white/15 transition-all duration-300 shadow-lg">
|
||||
<div className="text-3xl lg:text-4xl font-bold text-white mb-2">5</div>
|
||||
<div className="text-white/80 text-sm font-medium">Лет гарантии</div>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-6 text-center border border-white/20 hover:bg-white/15 transition-all duration-300 shadow-lg">
|
||||
<div className="text-3xl lg:text-4xl font-bold text-white mb-2">90%</div>
|
||||
<div className="text-white/80 text-sm font-medium">Клиентов рекомендуют нас</div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInSection>
|
||||
|
||||
{/* Right Form */}
|
||||
<FadeInSection as="div" className="w-full lg:w-96" delay={0.4}>
|
||||
<div className="bg-white/95 backdrop-blur-sm rounded-xl p-8 shadow-2xl">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-6 text-center">
|
||||
Получите расчет стоимости
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleFormSubmit} className="space-y-4">
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="Ваш телефон"
|
||||
value={phone}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value.replace(/\D/g, '').slice(0, 11);
|
||||
setPhone(val);
|
||||
}}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 text-gray-900 bg-white"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ваше имя"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 text-gray-900 bg-white"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 text-white py-4 rounded-lg hover:bg-blue-700 transition-colors text-lg font-semibold flex items-center justify-center group disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Отправка...' : 'Обсудить проект'}
|
||||
{!loading && (
|
||||
<svg className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500 text-white text-center py-3 px-4 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<p className="text-xs text-gray-500 mt-4 text-center">
|
||||
Нажимая кнопку "Обсудить проект", вы соглашаетесь с{' '}
|
||||
<a href="#" className="underline hover:text-blue-600">
|
||||
Политикой конфиденциальности
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</FadeInSection>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<AboutSection />
|
||||
<ProjectsSection onCatalogClick={() => setIsCatalogModalOpen(true)} />
|
||||
<WhyChooseUsSection />
|
||||
<TeamSection />
|
||||
<ReviewsSection onExcursionClick={() => setIsExcursionModalOpen(true)} />
|
||||
<ContactSection />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
157
src/components/AboutSection.tsx
Normal file
157
src/components/AboutSection.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import FadeInSection from './FadeInSection';
|
||||
import ContactModal from './ContactModal';
|
||||
|
||||
const AboutSection = () => {
|
||||
const [isContactModalOpen, setIsContactModalOpen] = useState(false);
|
||||
|
||||
const stats = [
|
||||
{ number: "15+", label: "Лет на рынке", icon: "🏗️" },
|
||||
{ number: "244", label: "Заказчика остались довольны", icon: "😊" },
|
||||
{ number: "5", label: "Проектов выполнено за последний месяц", icon: "⚡" },
|
||||
{ number: "100%", label: "Гарантия качества", icon: "✅" }
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: "🎯",
|
||||
title: "Индивидуальный подход",
|
||||
description: "Каждый проект разрабатывается с учетом пожеланий клиента"
|
||||
},
|
||||
{
|
||||
icon: "🛡️",
|
||||
title: "Качественные материалы",
|
||||
description: "Используем только проверенные материалы от надежных поставщиков"
|
||||
},
|
||||
{
|
||||
icon: "⏰",
|
||||
title: "Соблюдение сроков",
|
||||
description: "Строго придерживаемся согласованных временных рамок"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<section id="about" className="relative py-20 bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 overflow-hidden">
|
||||
{/* Статичный фон */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-500 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-purple-500 rounded-full blur-3xl"></div>
|
||||
<div className="absolute top-1/2 left-1/2 w-64 h-64 bg-cyan-500 rounded-full blur-3xl opacity-20"></div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
{/* Заголовок секции */}
|
||||
<FadeInSection as="div" className="text-center mb-16">
|
||||
<h2 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-white via-blue-100 to-white bg-clip-text text-transparent mb-4">
|
||||
О компании Ваш Дом
|
||||
</h2>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-blue-500 to-purple-500 mx-auto rounded-full"></div>
|
||||
</FadeInSection>
|
||||
|
||||
{/* Основной контент */}
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-center mb-20">
|
||||
{/* Изображение */}
|
||||
<FadeInSection as="div" className="relative group">
|
||||
<div className="relative h-[500px] w-full rounded-2xl overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-gray-900/80 via-transparent to-transparent z-10"></div>
|
||||
<Image
|
||||
src="/images/company.jpg"
|
||||
alt="О нашей компании"
|
||||
fill
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
{/* Декоративные элементы */}
|
||||
<div className="absolute -top-4 -left-4 w-24 h-24 bg-gradient-to-br from-blue-500/30 to-purple-500/30 rounded-full blur-xl"></div>
|
||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 bg-gradient-to-br from-purple-500/30 to-blue-500/30 rounded-full blur-xl"></div>
|
||||
</div>
|
||||
</FadeInSection>
|
||||
|
||||
{/* Текстовый контент */}
|
||||
<FadeInSection as="div" delay={0.2}>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-2xl font-bold text-white">
|
||||
Строим дома вашей мечты
|
||||
</h3>
|
||||
<p className="text-gray-300 leading-relaxed text-lg">
|
||||
Мы - команда профессионалов с более чем 10-летним опытом в строительстве
|
||||
современных домов. Наша миссия - создавать качественное и комфортное
|
||||
жилье для наших клиентов, используя передовые технологии и материалы.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Особенности */}
|
||||
<div className="space-y-4">
|
||||
{features.map((feature, index) => (
|
||||
<FadeInSection key={index} delay={0.3 + index * 0.1}>
|
||||
<div className="flex items-start space-x-4 p-4 rounded-xl bg-white/5 backdrop-blur-sm border border-white/10 hover:bg-white/10 transition-all duration-300 group">
|
||||
<div className="text-2xl group-hover:scale-110 transition-transform duration-300">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-white mb-1">{feature.title}</h4>
|
||||
<p className="text-gray-400 text-sm">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInSection>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FadeInSection>
|
||||
</div>
|
||||
|
||||
{/* Статистика */}
|
||||
<FadeInSection as="div" delay={0.4}>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{stats.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="group relative p-6 rounded-2xl bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-md border border-white/20 hover:border-white/40 transition-all duration-500 hover:scale-105 hover:-translate-y-2"
|
||||
>
|
||||
{/* Фон */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-blue-500/20 to-purple-500/20 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="relative z-10 text-center">
|
||||
<div className="text-3xl mb-2 group-hover:scale-110 transition-transform duration-300">
|
||||
{stat.icon}
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-white mb-2 group-hover:text-blue-300 transition-all duration-300 drop-shadow-lg">
|
||||
{stat.number}
|
||||
</div>
|
||||
<div className="text-gray-300 text-sm font-medium">
|
||||
{stat.label}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Декоративный элемент */}
|
||||
<div className="absolute top-2 right-2 w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full opacity-50 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FadeInSection>
|
||||
|
||||
{/* Призыв к действию */}
|
||||
<FadeInSection as="div" delay={0.6} className="text-center mt-16">
|
||||
<button
|
||||
onClick={() => setIsContactModalOpen(true)}
|
||||
className="inline-flex items-center space-x-2 px-6 py-3 rounded-full bg-gradient-to-r from-blue-500/20 to-purple-500/20 border border-white/20 backdrop-blur-sm hover:scale-105 transition-transform duration-300 cursor-pointer"
|
||||
>
|
||||
<span className="text-white font-medium">Готовы начать строительство?</span>
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
|
||||
</button>
|
||||
</FadeInSection>
|
||||
</div>
|
||||
|
||||
{/* Модальное окно */}
|
||||
<ContactModal
|
||||
isOpen={isContactModalOpen}
|
||||
onClose={() => setIsContactModalOpen(false)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutSection;
|
139
src/components/CallbackModal.tsx
Normal file
139
src/components/CallbackModal.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { X, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
|
||||
interface CallbackModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const CallbackModal = ({ isOpen, onClose }: CallbackModalProps) => {
|
||||
const [name, setName] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const validatePhone = (value: string) => {
|
||||
const digits = value.replace(/\D/g, '');
|
||||
if (digits.length !== 11) return false;
|
||||
if (!(digits.startsWith('7') || digits.startsWith('8'))) return false;
|
||||
if (/^(7|8)0{10}$/.test(digits)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateName = (value: string) => {
|
||||
return /^[А-Яа-яA-Za-zЁё\-]{2,}$/.test(value.trim());
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
if (!validateName(name)) return setError('Пожалуйста, укажите корректное имя (только буквы, не менее 2 символов)');
|
||||
if (!validatePhone(phone)) return setError('Пожалуйста, укажите корректный российский номер телефона');
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/send-telegram', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, phone, message, material: 'Звонок' }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSuccess(true);
|
||||
setName('');
|
||||
setPhone('');
|
||||
setMessage('');
|
||||
} else {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
}
|
||||
} catch {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setName('');
|
||||
setPhone('');
|
||||
setMessage('');
|
||||
setError('');
|
||||
setSuccess(false);
|
||||
setLoading(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-900/50 z-50 flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg p-8 max-w-md w-full mx-4 relative">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||||
Заказать звонок
|
||||
</h2>
|
||||
|
||||
{!success ? (
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ваше имя"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="Ваш телефон"
|
||||
value={phone}
|
||||
onChange={e => {
|
||||
const val = e.target.value.replace(/\D/g, '').slice(0, 11);
|
||||
setPhone(val);
|
||||
}}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Ваше сообщение (необязательно)"
|
||||
rows={3}
|
||||
value={message}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||||
/>
|
||||
{error && (
|
||||
<div className="bg-red-500 text-white text-center py-3 px-4 rounded-lg flex items-center justify-center min-h-[48px] md:min-h-[40px] md:text-base text-sm whitespace-pre-line">
|
||||
<AlertCircle className="w-5 h-5 mr-2 shrink-0" />
|
||||
<span className="block w-full break-words">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition-colors font-semibold disabled:opacity-60"
|
||||
>
|
||||
{loading ? 'Отправка...' : 'Отправить'}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 animate-fadeIn">
|
||||
<div className="bg-green-100 rounded-2xl p-4 mb-6 flex items-center shadow-lg w-full">
|
||||
<CheckCircle className="w-8 h-8 text-green-600 mr-2" />
|
||||
<span className="text-green-700 text-lg font-semibold leading-snug text-left">
|
||||
Спасибо! Данные успешно отправлены.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallbackModal;
|
140
src/components/CatalogRequestModal.tsx
Normal file
140
src/components/CatalogRequestModal.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { X, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
|
||||
interface CatalogRequestModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const CatalogRequestModal = ({ isOpen, onClose }: CatalogRequestModalProps) => {
|
||||
const [phone, setPhone] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const validatePhone = (value: string) => {
|
||||
const digits = value.replace(/\D/g, '');
|
||||
if (digits.length !== 11) return false;
|
||||
if (!(digits.startsWith('7') || digits.startsWith('8'))) return false;
|
||||
if (/^(7|8)0{10}$/.test(digits)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateName = (value: string) => {
|
||||
return /^[А-Яа-яA-Za-zЁё\-]{2,}$/.test(value.trim());
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
const nameValid = validateName(name);
|
||||
const phoneValid = validatePhone(phone);
|
||||
if (!phoneValid) return setError('Пожалуйста, укажите корректный российский номер телефона');
|
||||
if (!nameValid) return setError('Пожалуйста, укажите корректное имя (только буквы, не менее 2 символов)');
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/send-telegram', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, phone, material: 'Каталог', area: '-', finish: '-', finance: '-' }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSuccess(true);
|
||||
} else {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
}
|
||||
} catch {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setPhone('');
|
||||
setName('');
|
||||
setError('');
|
||||
setSuccess(false);
|
||||
setLoading(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/50">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 relative animate-fadeIn">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-700"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="p-8 md:p-10 flex flex-col items-center">
|
||||
<div className="w-full flex justify-center mb-6">
|
||||
<Image src="/images/katalog.png" alt="Каталог проектов" width={320} height={120} className="object-contain rounded-lg shadow-md" />
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-center mb-2">Укажите контакты</h2>
|
||||
<p className="text-gray-500 text-center mb-8">И мы отправим каталог проектов на WhatsApp</p>
|
||||
|
||||
{!success ? (
|
||||
<form onSubmit={handleSubmit} className="w-full flex flex-col gap-4">
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="+7 (___) ___-__-__"
|
||||
value={phone}
|
||||
onChange={e => {
|
||||
const val = e.target.value.replace(/\D/g, '').slice(0, 11);
|
||||
setPhone(val);
|
||||
}}
|
||||
className="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 text-lg"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Имя"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 text-lg"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-800 text-white py-3 rounded-lg hover:bg-blue-900 transition-colors text-lg font-semibold mt-2 disabled:opacity-60"
|
||||
>
|
||||
{loading ? 'Отправка...' : 'Хочу проект'}
|
||||
</button>
|
||||
{error && (
|
||||
<div className="bg-red-500 text-white text-center py-3 px-4 rounded-lg flex items-center justify-center min-h-[48px] md:min-h-[40px] md:text-base text-sm whitespace-pre-line mt-2">
|
||||
<AlertCircle className="w-5 h-5 mr-2 shrink-0" />
|
||||
<span className="block w-full break-words">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
) : (
|
||||
<div className="w-full flex flex-col items-center animate-fadeIn">
|
||||
<div className="bg-green-100 rounded-2xl p-6 mb-8 flex items-center shadow-lg w-full">
|
||||
<CheckCircle className="w-10 h-10 text-green-600 mr-4 flex-shrink-0" />
|
||||
<span className="text-green-700 text-lg md:text-xl font-semibold leading-snug text-left">
|
||||
Спасибо! Данные успешно отправлены.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="bg-blue-800 text-white px-8 py-3 rounded-full hover:bg-blue-900 transition-colors text-lg font-semibold shadow-md"
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CatalogRequestModal;
|
194
src/components/ContactModal.tsx
Normal file
194
src/components/ContactModal.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ContactModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ContactModal = ({ isOpen, onClose }: ContactModalProps) => {
|
||||
const [formData, setFormData] = useState({
|
||||
phone: '',
|
||||
name: ''
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({
|
||||
phone: '',
|
||||
name: ''
|
||||
});
|
||||
|
||||
const validatePhone = (phone: string) => {
|
||||
const phoneRegex = /^[78]\d{10}$/;
|
||||
return phoneRegex.test(phone.replace(/\D/g, ''));
|
||||
};
|
||||
|
||||
const validateName = (name: string) => {
|
||||
const nameRegex = /^[а-яёА-ЯЁa-zA-Z\s]{2,}$/;
|
||||
return nameRegex.test(name.trim());
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
// Очищаем ошибки при вводе
|
||||
if (errors[field as keyof typeof errors]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = e.target.value.replace(/\D/g, '');
|
||||
|
||||
if (value.length > 11) {
|
||||
value = value.slice(0, 11);
|
||||
}
|
||||
|
||||
if (value.length > 0 && !value.startsWith('7') && !value.startsWith('8')) {
|
||||
value = '7' + value;
|
||||
}
|
||||
|
||||
let formattedValue = value;
|
||||
if (value.length > 1) {
|
||||
formattedValue = `+${value.slice(0, 1)} (${value.slice(1, 4)}) ${value.slice(4, 7)}-${value.slice(7, 9)}-${value.slice(9, 11)}`;
|
||||
}
|
||||
|
||||
handleInputChange('phone', formattedValue);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const newErrors = {
|
||||
phone: '',
|
||||
name: ''
|
||||
};
|
||||
|
||||
const cleanPhone = formData.phone.replace(/\D/g, '');
|
||||
|
||||
if (!validatePhone(cleanPhone)) {
|
||||
newErrors.phone = 'Введите корректный номер телефона';
|
||||
}
|
||||
|
||||
if (!validateName(formData.name)) {
|
||||
newErrors.name = 'Имя должно содержать только буквы (минимум 2 символа)';
|
||||
}
|
||||
|
||||
if (newErrors.phone || newErrors.name) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/send-telegram', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'contact',
|
||||
name: formData.name,
|
||||
phone: formData.phone,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setFormData({ phone: '', name: '' });
|
||||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка отправки:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="relative w-full max-w-md">
|
||||
{/* Фоновые декоративные элементы */}
|
||||
<div className="absolute -top-4 -left-4 w-24 h-24 bg-gradient-to-br from-blue-500/30 to-purple-500/30 rounded-full blur-xl"></div>
|
||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 bg-gradient-to-br from-purple-500/30 to-blue-500/30 rounded-full blur-xl"></div>
|
||||
|
||||
{/* Основной контейнер */}
|
||||
<div className="relative bg-gradient-to-br from-gray-900/95 to-gray-800/95 backdrop-blur-md border border-white/20 rounded-2xl p-8 shadow-2xl">
|
||||
{/* Кнопка закрытия */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 w-8 h-8 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition-colors duration-200 text-white"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
{/* Заголовок */}
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-2xl md:text-3xl font-bold bg-gradient-to-r from-white via-blue-100 to-white bg-clip-text text-transparent mb-2">
|
||||
Укажите свои данные
|
||||
</h2>
|
||||
<p className="text-gray-300 text-sm">
|
||||
И наш менеджер свяжется с Вами в ближайшее время
|
||||
</p>
|
||||
<div className="w-16 h-1 bg-gradient-to-r from-blue-500 to-purple-500 mx-auto rounded-full mt-4"></div>
|
||||
</div>
|
||||
|
||||
{/* Форма */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Поле телефона */}
|
||||
<div>
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={handlePhoneChange}
|
||||
placeholder="+7 (999) 999-99-99"
|
||||
className="w-full px-4 py-4 bg-white/5 backdrop-blur-sm border border-white/20 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:border-blue-400 focus:bg-white/10 transition-all duration-300"
|
||||
/>
|
||||
{errors.phone && (
|
||||
<p className="text-red-400 text-sm mt-2 ml-1">{errors.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Поле имени */}
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
placeholder="Имя"
|
||||
className="w-full px-4 py-4 bg-white/5 backdrop-blur-sm border border-white/20 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:border-blue-400 focus:bg-white/10 transition-all duration-300"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-red-400 text-sm mt-2 ml-1">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Кнопка отправки */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full py-4 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 disabled:from-gray-600 disabled:to-gray-700 text-white font-semibold rounded-xl transition-all duration-300 transform hover:scale-105 disabled:scale-100 disabled:cursor-not-allowed shadow-lg"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
||||
<span>Отправка...</span>
|
||||
</div>
|
||||
) : (
|
||||
'Заказать звонок'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Декоративные элементы */}
|
||||
<div className="absolute top-2 right-12 w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full opacity-50"></div>
|
||||
<div className="absolute bottom-2 left-8 w-1 h-1 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full opacity-60"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactModal;
|
184
src/components/ContactSection.tsx
Normal file
184
src/components/ContactSection.tsx
Normal file
@ -0,0 +1,184 @@
|
||||
import { useState } from 'react';
|
||||
import { Phone, Mail, MapPin, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import FadeInSection from './FadeInSection';
|
||||
|
||||
const ContactSection = () => {
|
||||
const [name, setName] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const validatePhone = (value: string) => {
|
||||
const digits = value.replace(/\D/g, '');
|
||||
if (digits.length !== 11) return false;
|
||||
if (!(digits.startsWith('7') || digits.startsWith('8'))) return false;
|
||||
if (/^(7|8)0{10}$/.test(digits)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateName = (value: string) => {
|
||||
return /^[А-Яа-яA-Za-zЁё\-]{2,}$/.test(value.trim());
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
if (!validateName(name)) return setError('Пожалуйста, укажите корректное имя (только буквы, не менее 2 символов)');
|
||||
if (!validatePhone(phone)) return setError('Пожалуйста, укажите корректный российский номер телефона');
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/send-telegram', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, phone, material: 'Контакты', area: '-', finish: '-', finance: '-', message }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSuccess(true);
|
||||
setName('');
|
||||
setPhone('');
|
||||
setMessage('');
|
||||
} else {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
}
|
||||
} catch {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="contacts" className="py-20 bg-white">
|
||||
<div className="container mx-auto px-4">
|
||||
<FadeInSection as="h2" className="text-3xl font-bold text-gray-900 mb-12 text-center">
|
||||
Наши контакты
|
||||
</FadeInSection>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<FadeInSection as="div" delay={0.2}>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start">
|
||||
<Phone className="w-6 h-6 text-blue-600 mt-1 mr-4" />
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-1">
|
||||
Телефон
|
||||
</h3>
|
||||
<a
|
||||
href="tel:+79672123132"
|
||||
className="text-gray-600 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
+7 (967) 212-31-32
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<Mail className="w-6 h-6 text-blue-600 mt-1 mr-4" />
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-1">
|
||||
Email
|
||||
</h3>
|
||||
<a
|
||||
href="mailto:sksdstroy@yandex.ru"
|
||||
className="text-gray-600 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
sksdstroy@yandex.ru
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start">
|
||||
<MapPin className="w-6 h-6 text-blue-600 mt-1 mr-4" />
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-1">
|
||||
Адрес
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Чувашская республика, г. Чебоксары,<br />
|
||||
ул. Пирогова, 1 корп. 3, 428034
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">
|
||||
Реквизиты
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
ИП Степанов Денис Сергеевич<br />
|
||||
ИНН 212306083987
|
||||
</p>
|
||||
</div>
|
||||
</FadeInSection>
|
||||
|
||||
<FadeInSection as="div" className="bg-gray-50 p-6 rounded-lg" delay={0.4}>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-6">
|
||||
Отправить сообщение
|
||||
</h3>
|
||||
{!success ? (
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ваше имя"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="Ваш телефон"
|
||||
value={phone}
|
||||
onChange={e => {
|
||||
const val = e.target.value.replace(/\D/g, '').slice(0, 11);
|
||||
setPhone(val);
|
||||
}}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
placeholder="Ваше сообщение"
|
||||
rows={4}
|
||||
value={message}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="bg-red-500 text-white text-center py-3 px-4 rounded-lg flex items-center justify-center min-h-[48px] md:min-h-[40px] md:text-base text-sm whitespace-pre-line">
|
||||
<AlertCircle className="w-5 h-5 mr-2 shrink-0" />
|
||||
<span className="block w-full break-words">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition-colors font-semibold disabled:opacity-60"
|
||||
>
|
||||
{loading ? 'Отправка...' : 'Отправить'}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 animate-fadeIn">
|
||||
<div className="bg-green-100 rounded-2xl p-4 mb-6 flex items-center shadow-lg w-full">
|
||||
<CheckCircle className="w-8 h-8 text-green-600 mr-2" />
|
||||
<span className="text-green-700 text-lg font-semibold leading-snug text-left">
|
||||
Спасибо! Ваше сообщение успешно отправлено.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</FadeInSection>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactSection;
|
137
src/components/ExcursionModal.tsx
Normal file
137
src/components/ExcursionModal.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { X, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
|
||||
interface ExcursionModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ExcursionModal = ({ isOpen, onClose }: ExcursionModalProps) => {
|
||||
const [name, setName] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const validatePhone = (value: string) => {
|
||||
const digits = value.replace(/\D/g, '');
|
||||
if (digits.length !== 11) return false;
|
||||
if (!(digits.startsWith('7') || digits.startsWith('8'))) return false;
|
||||
if (/^(7|8)0{10}$/.test(digits)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateName = (value: string) => {
|
||||
return /^[А-Яа-яA-Za-zЁё\-]{2,}$/.test(value.trim());
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
if (!name && !phone) return setError('Ни одно поле не заполнено');
|
||||
if (!validateName(name)) return setError('Пожалуйста, укажите корректное имя (только буквы, не менее 2 символов)');
|
||||
if (!validatePhone(phone)) return setError('Пожалуйста, укажите корректный российский номер телефона');
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/send-telegram', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, phone, material: 'Экскурсия', area: '-', finish: '-', finance: '-' }),
|
||||
});
|
||||
if (res.ok) {
|
||||
setSuccess(true);
|
||||
} else {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
}
|
||||
} catch {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setName('');
|
||||
setPhone('');
|
||||
setError('');
|
||||
setSuccess(false);
|
||||
setLoading(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/50">
|
||||
<div className="bg-[#fdf7f2] rounded-2xl shadow-2xl w-full max-w-xl mx-4 relative animate-fadeIn">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-700"
|
||||
aria-label="Закрыть"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="p-8 md:p-12 flex flex-col items-center">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4 text-center">Записаться на экскурсию</h2>
|
||||
<p className="text-gray-600 text-center mb-8 text-lg max-w-xl">
|
||||
Мы регулярно проводим экскурсии на готовые объекты для знакомства с компанией и технологиями строительства
|
||||
</p>
|
||||
|
||||
{!success ? (
|
||||
<form onSubmit={handleSubmit} className="w-full flex flex-col gap-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ваше полное имя"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
className="w-full px-4 py-3 border-b border-gray-400 bg-transparent focus:outline-none focus:border-blue-700 text-lg placeholder-gray-400"
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="Ваш телефон"
|
||||
value={phone}
|
||||
onChange={e => {
|
||||
const val = e.target.value.replace(/\D/g, '').slice(0, 11);
|
||||
setPhone(val);
|
||||
}}
|
||||
className="w-full px-4 py-3 border-b border-gray-400 bg-transparent focus:outline-none focus:border-blue-700 text-lg placeholder-gray-400"
|
||||
/>
|
||||
{error && (
|
||||
<div className="bg-red-500 text-white text-center py-3 px-4 rounded-lg flex items-center justify-center min-h-[48px] md:min-h-[40px] md:text-base text-sm whitespace-pre-line">
|
||||
<AlertCircle className="w-5 h-5 mr-2 shrink-0" />
|
||||
<span className="block w-full break-words">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-900 text-white py-3 rounded-full hover:bg-blue-800 transition-colors text-lg font-semibold mt-2 disabled:opacity-60 shadow-md"
|
||||
>
|
||||
{loading ? 'Отправка...' : 'Отправить заявку'}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="w-full flex flex-col items-center animate-fadeIn">
|
||||
<div className="bg-green-100 rounded-2xl p-6 mb-8 flex items-center shadow-lg w-full">
|
||||
<CheckCircle className="w-10 h-10 text-green-600 mr-4 flex-shrink-0" />
|
||||
<span className="text-green-700 text-lg md:text-xl font-semibold leading-snug text-left">
|
||||
Спасибо! Ваша заявка успешно отправлена.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="bg-blue-900 text-white px-8 py-3 rounded-full hover:bg-blue-800 transition-colors text-lg font-semibold shadow-md"
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExcursionModal;
|
32
src/components/FadeInOnMount.tsx
Normal file
32
src/components/FadeInOnMount.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { motion, HTMLMotionProps } from 'framer-motion';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface FadeInOnMountProps {
|
||||
children: ReactNode;
|
||||
as?: keyof typeof motion;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
const FadeInOnMount = ({
|
||||
children,
|
||||
as = 'div',
|
||||
className = '',
|
||||
delay = 0,
|
||||
}: FadeInOnMountProps) => {
|
||||
const MotionComponent = motion[as] as React.ComponentType<HTMLMotionProps<"div">>;
|
||||
return (
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay, ease: [0.21, 0.47, 0.32, 0.98] }}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</MotionComponent>
|
||||
);
|
||||
};
|
||||
|
||||
export default FadeInOnMount;
|
38
src/components/FadeInSection.tsx
Normal file
38
src/components/FadeInSection.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { motion, HTMLMotionProps } from 'framer-motion';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface FadeInSectionProps {
|
||||
children: ReactNode;
|
||||
as?: keyof typeof motion;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
const FadeInSection = ({
|
||||
children,
|
||||
as = 'div',
|
||||
className = '',
|
||||
delay = 0
|
||||
}: FadeInSectionProps) => {
|
||||
const MotionComponent = motion[as] as React.ComponentType<HTMLMotionProps<"div">>;
|
||||
|
||||
return (
|
||||
<MotionComponent
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{
|
||||
duration: 0.6,
|
||||
delay: delay,
|
||||
ease: [0.21, 0.47, 0.32, 0.98]
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</MotionComponent>
|
||||
);
|
||||
};
|
||||
|
||||
export default FadeInSection;
|
92
src/components/Footer.tsx
Normal file
92
src/components/Footer.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="bg-gray-900 text-white py-12">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-4">ВашДом</h3>
|
||||
<p className="text-gray-400">
|
||||
Современное строительство домов и коттеджей под ключ. Индивидуальные проекты, качественные материалы, профессиональная команда.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg font-bold mb-4">Навигация</h4>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link href="#about" className="text-gray-400 hover:text-white transition-colors">
|
||||
О компании
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="#projects" className="text-gray-400 hover:text-white transition-colors">
|
||||
Проекты
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="#services" className="text-gray-400 hover:text-white transition-colors">
|
||||
Услуги
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="#reviews" className="text-gray-400 hover:text-white transition-colors">
|
||||
Отзывы
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="#contacts" className="text-gray-400 hover:text-white transition-colors">
|
||||
Контакты
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg font-bold mb-4">Контакты</h4>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li>+7 (900) 123-45-67</li>
|
||||
<li>info@vashdom.ru</li>
|
||||
<li>
|
||||
г. Москва,<br />
|
||||
ул. Новая, 10
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-lg font-bold mb-4">Мы в соцсетях</h4>
|
||||
<div className="flex space-x-4">
|
||||
<a
|
||||
href="#"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Telegram
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
WhatsApp
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
|
||||
<p>© 2025 ВашДом. Все права защищены.</p>
|
||||
<p className="mt-2 text-sm">
|
||||
Разработка сайта <a href="https://biveki.ru/" target="_blank" rel="noopener noreferrer" className="underline hover:text-white">BivekiGroup</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
94
src/components/Header.tsx
Normal file
94
src/components/Header.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Menu } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import CallbackModal from './CallbackModal';
|
||||
import MobileMenu from './MobileMenu';
|
||||
import FadeInOnMount from './FadeInOnMount';
|
||||
|
||||
const Header = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FadeInOnMount as="header" className={`fixed w-full z-50 bg-gray-900/95 backdrop-blur-sm py-4 shadow-lg`}>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center space-x-3 flex-shrink-0 py-1">
|
||||
<div className="bg-blue-600 p-2 rounded">
|
||||
<svg className="w-8 h-8 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M2 12L12 2L22 12V22H16V16H8V22H2V12Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-white">
|
||||
<div className="text-xl font-bold">SD</div>
|
||||
<div className="text-sm font-semibold -mt-1">STROY</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="hidden lg:flex items-center space-x-8">
|
||||
<Link href="#services" className="text-white hover:text-blue-400 transition-colors text-sm font-medium py-1">
|
||||
Услуги
|
||||
</Link>
|
||||
<Link href="#projects" className="text-white hover:text-blue-400 transition-colors text-sm font-medium py-1">
|
||||
Каталог домов
|
||||
</Link>
|
||||
<Link href="#mortgage" className="text-white hover:text-blue-400 transition-colors text-sm font-medium py-1">
|
||||
Ипотека
|
||||
</Link>
|
||||
<Link href="#about" className="text-white hover:text-blue-400 transition-colors text-sm font-medium py-1">
|
||||
О нас
|
||||
</Link>
|
||||
<Link href="#contacts" className="text-white hover:text-blue-400 transition-colors text-sm font-medium py-1">
|
||||
Контакты
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* Right side */}
|
||||
<div className="hidden md:flex items-center space-x-6 flex-shrink-0">
|
||||
<div className="text-right">
|
||||
<a
|
||||
href="tel:+78352329226"
|
||||
className="flex items-center text-white hover:text-blue-400 transition-colors group text-lg font-semibold whitespace-nowrap"
|
||||
>
|
||||
+7 8352 32 92 26
|
||||
</a>
|
||||
<div className="text-xs text-gray-300 mt-1">Пн - Вс с 8:00 до 20:00</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="bg-white text-gray-900 px-6 py-2.5 rounded-lg hover:bg-gray-100 transition-colors hover:shadow-lg text-sm font-semibold flex-shrink-0"
|
||||
>
|
||||
Оставить заявку
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(true)}
|
||||
className="md:hidden text-white hover:text-gray-300 transition-colors p-2 hover:bg-white/10 rounded-lg"
|
||||
aria-label="Открыть меню"
|
||||
>
|
||||
<Menu className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInOnMount>
|
||||
|
||||
<CallbackModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
|
||||
<MobileMenu
|
||||
isOpen={isMobileMenuOpen}
|
||||
onClose={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
302
src/components/HouseCalculatorModal.tsx
Normal file
302
src/components/HouseCalculatorModal.tsx
Normal file
@ -0,0 +1,302 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { X, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
|
||||
const materials = [
|
||||
{ label: 'Кирпич/керамический блок', value: 'brick', img: '/images/keramic.jpg' },
|
||||
{ label: 'Газобетон', value: 'aerated', img: '/images/gazobet.png' },
|
||||
{ label: 'Керамзитобетон', value: 'claydite', img: '/images/keramiz.jpg' },
|
||||
];
|
||||
|
||||
const areas = [
|
||||
'80-100 кв.м.',
|
||||
'100-150 кв.м.',
|
||||
'150-200 кв.м.',
|
||||
'более 200 кв.м.',
|
||||
];
|
||||
|
||||
const finishes = [
|
||||
'Без отделки',
|
||||
'Черновая отделка (стяжка, штукатурка и тд)',
|
||||
'Чистовая отделка (обои, ламинат и тд)',
|
||||
];
|
||||
|
||||
const finances = [
|
||||
'Наличные',
|
||||
'Сельская ипотека',
|
||||
'Ипотека, кредит',
|
||||
'Свой вариант',
|
||||
];
|
||||
|
||||
interface HouseCalculatorModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
userName?: string;
|
||||
userPhone?: string;
|
||||
}
|
||||
|
||||
const HouseCalculatorModal = ({ isOpen, onClose, userName = '', userPhone = '' }: HouseCalculatorModalProps) => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [material, setMaterial] = useState('');
|
||||
const [area, setArea] = useState('');
|
||||
const [finish, setFinish] = useState('');
|
||||
const [finance, setFinance] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleNext = () => {
|
||||
setError('');
|
||||
if (step === 1 && !material) return setError('Выберите материал');
|
||||
if (step === 2 && !area) return setError('Выберите площадь');
|
||||
if (step === 3 && !finish) return setError('Выберите вариант отделки');
|
||||
if (step === 4 && !finance) {
|
||||
setError('Выберите источник финансирования');
|
||||
return;
|
||||
}
|
||||
if (step === 4) {
|
||||
// После 4-го шага отправляем данные в Telegram и переходим к 5-му шагу
|
||||
handleSubmitCalculator();
|
||||
return;
|
||||
}
|
||||
setStep((s) => s + 1);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
setError('');
|
||||
setStep((s) => s - 1);
|
||||
};
|
||||
|
||||
const handleSubmitCalculator = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/send-telegram', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
material,
|
||||
area,
|
||||
finish,
|
||||
finance,
|
||||
name: userName || 'Из калькулятора',
|
||||
phone: userPhone || 'Не указан'
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setStep(5); // Переходим к 5-му шагу
|
||||
} else {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
}
|
||||
} catch {
|
||||
setError('Ошибка отправки. Попробуйте позже.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const closeModal = () => {
|
||||
setStep(1);
|
||||
setMaterial('');
|
||||
setArea('');
|
||||
setFinish('');
|
||||
setFinance('');
|
||||
setError('');
|
||||
setSuccess(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl mx-auto relative animate-fadeIn max-h-[90vh] overflow-y-auto">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-700 p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||
aria-label="Закрыть калькулятор"
|
||||
>
|
||||
<X className="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
</button>
|
||||
<div className="p-4 sm:p-6 md:p-8">
|
||||
<div className="flex items-center justify-between mb-4 sm:mb-6">
|
||||
<span className="text-gray-500 text-xs sm:text-sm">
|
||||
{step < 5 ? 'Для расчета стоимости выберите один из вариантов' : 'Спасибо за заявку!'}
|
||||
</span>
|
||||
<span className="text-gray-500 text-xs sm:text-sm">{step}/5</span>
|
||||
</div>
|
||||
<div className="w-full h-1 bg-gray-200 rounded mb-6 sm:mb-8">
|
||||
<div className="h-1 bg-blue-600 rounded transition-all" style={{ width: `${(step-1)*25}%` }} />
|
||||
</div>
|
||||
|
||||
{step === 1 && (
|
||||
<div>
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Из какого материала хотите построить дом?</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||
{materials.map((m) => (
|
||||
<button
|
||||
key={m.value}
|
||||
type="button"
|
||||
onClick={() => setMaterial(m.value)}
|
||||
className={`group border-2 rounded-xl p-3 sm:p-4 flex flex-col items-center transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-600 relative ${
|
||||
material === m.value ? 'border-blue-600 shadow-lg' : 'border-gray-200 hover:border-blue-400'
|
||||
}`}
|
||||
>
|
||||
<div className="relative w-20 h-20 sm:w-24 sm:h-24 mb-3 sm:mb-4">
|
||||
<Image
|
||||
src={m.img}
|
||||
alt={m.label}
|
||||
fill
|
||||
className="object-contain rounded-lg"
|
||||
sizes="(max-width: 640px) 80px, (max-width: 1024px) 96px, 96px"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-base sm:text-lg font-medium text-gray-800 text-center">{m.label}</span>
|
||||
{material === m.value && (
|
||||
<CheckCircle className="w-5 h-5 sm:w-6 sm:h-6 text-blue-600 absolute top-2 right-2" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div>
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Какая площадь дома?</h2>
|
||||
<div className="flex flex-col gap-3 sm:gap-4">
|
||||
{areas.map((a) => (
|
||||
<label key={a} className="flex items-center cursor-pointer p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<input
|
||||
type="radio"
|
||||
name="area"
|
||||
value={a}
|
||||
checked={area === a}
|
||||
onChange={() => setArea(a)}
|
||||
className="accent-blue-600 w-4 h-4 sm:w-5 sm:h-5 mr-3"
|
||||
/>
|
||||
<span className="text-base sm:text-lg text-gray-800">{a}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div>
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Вариант отделки</h2>
|
||||
<div className="flex flex-col gap-3 sm:gap-4">
|
||||
{finishes.map((f) => (
|
||||
<label key={f} className="flex items-center cursor-pointer p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<input
|
||||
type="radio"
|
||||
name="finish"
|
||||
value={f}
|
||||
checked={finish === f}
|
||||
onChange={() => setFinish(f)}
|
||||
className="accent-blue-600 w-4 h-4 sm:w-5 sm:h-5 mr-3"
|
||||
/>
|
||||
<span className="text-base sm:text-lg text-gray-800">{f}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 4 && (
|
||||
<div>
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Источник финансирования</h2>
|
||||
<div className="flex flex-col gap-3 sm:gap-4">
|
||||
{finances.map((f) => (
|
||||
<label key={f} className="flex items-center cursor-pointer p-3 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<input
|
||||
type="radio"
|
||||
name="finance"
|
||||
value={f}
|
||||
checked={finance === f}
|
||||
onChange={() => setFinance(f)}
|
||||
className="accent-blue-600 w-4 h-4 sm:w-5 sm:h-5 mr-3"
|
||||
/>
|
||||
<span className="text-base sm:text-lg text-gray-800">{f}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 5 && (
|
||||
<div className="flex flex-col items-center justify-center py-8 sm:py-12 animate-fadeIn">
|
||||
<div className="bg-green-100 rounded-2xl p-4 sm:p-6 mb-6 sm:mb-8 flex items-center shadow-lg w-full max-w-xl mx-auto">
|
||||
<CheckCircle className="w-8 h-8 sm:w-10 sm:h-10 text-green-600 mr-3 sm:mr-4 flex-shrink-0" />
|
||||
<span className="text-green-700 text-base sm:text-lg md:text-xl font-semibold leading-snug text-left">
|
||||
Благодарим за обращение в нашу компанию!<br className='hidden md:block' /> В течение 15 минут мы свяжемся с вами!
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="bg-blue-600 text-white px-6 sm:px-8 py-3 rounded-full hover:bg-blue-700 transition-colors text-base sm:text-lg font-semibold shadow-md"
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="flex flex-col items-center justify-center py-8 sm:py-12 animate-fadeIn">
|
||||
<div className="bg-green-100 rounded-2xl p-4 sm:p-6 mb-6 sm:mb-8 flex items-center shadow-lg w-full max-w-xl mx-auto">
|
||||
<CheckCircle className="w-8 h-8 sm:w-10 sm:h-10 text-green-600 mr-3 sm:mr-4 flex-shrink-0" />
|
||||
<span className="text-green-700 text-base sm:text-lg md:text-xl font-semibold leading-snug text-left">
|
||||
Благодарим за обращение в нашу компанию!<br className='hidden md:block' /> В течение 15 минут мы свяжемся с вами!
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="bg-blue-600 text-white px-6 sm:px-8 py-3 rounded-full hover:bg-blue-700 transition-colors text-base sm:text-lg font-semibold shadow-md"
|
||||
>
|
||||
Закрыть
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 sm:mt-6 bg-red-500 text-white text-center py-2 sm:py-3 px-3 sm:px-4 rounded-lg flex items-center justify-center min-h-[40px] sm:min-h-[48px] text-sm sm:text-base whitespace-pre-line">
|
||||
<AlertCircle className="w-4 h-4 sm:w-5 sm:h-5 mr-2 shrink-0" />
|
||||
<span className="block w-full break-words">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!success && step < 5 && (
|
||||
<div className="flex justify-between mt-6 sm:mt-8">
|
||||
{step > 1 && (
|
||||
<button
|
||||
onClick={handlePrev}
|
||||
className="bg-gray-200 text-gray-700 px-4 sm:px-6 py-2 rounded-full hover:bg-gray-300 transition-colors text-sm sm:text-base font-medium"
|
||||
>
|
||||
← Назад
|
||||
</button>
|
||||
)}
|
||||
{step < 4 && (
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="ml-auto bg-blue-600 text-white px-6 sm:px-8 py-2 rounded-full hover:bg-blue-700 transition-colors text-sm sm:text-base font-medium"
|
||||
>
|
||||
Далее →
|
||||
</button>
|
||||
)}
|
||||
{step === 4 && (
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="ml-auto bg-blue-600 text-white px-6 sm:px-8 py-2 rounded-full hover:bg-blue-700 transition-colors text-sm sm:text-base font-medium"
|
||||
>
|
||||
Рассчитать стоимость
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HouseCalculatorModal;
|
109
src/components/MobileMenu.tsx
Normal file
109
src/components/MobileMenu.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { X, Phone } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface MobileMenuProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const MobileMenu = ({ isOpen, onClose }: MobileMenuProps) => {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-900 z-50 animate-fadeIn">
|
||||
<div className="container mx-auto px-4 py-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<Link href="/" className="flex items-center space-x-3">
|
||||
<div className="bg-blue-600 p-2 rounded">
|
||||
<svg className="w-8 h-8 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M2 12L12 2L22 12V22H16V16H8V22H2V12Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-white">
|
||||
<div className="text-xl font-bold">SD</div>
|
||||
<div className="text-sm font-semibold -mt-1">STROY</div>
|
||||
</div>
|
||||
</Link>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white hover:text-gray-300 p-2 hover:bg-white/10 rounded-lg transition-colors"
|
||||
aria-label="Закрыть меню"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 flex flex-col space-y-6">
|
||||
<Link
|
||||
href="#services"
|
||||
className="text-xl text-white hover:text-blue-400 transition-colors py-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Услуги
|
||||
</Link>
|
||||
<Link
|
||||
href="#projects"
|
||||
className="text-xl text-white hover:text-blue-400 transition-colors py-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Каталог домов
|
||||
</Link>
|
||||
<Link
|
||||
href="#mortgage"
|
||||
className="text-xl text-white hover:text-blue-400 transition-colors py-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Ипотека
|
||||
</Link>
|
||||
<Link
|
||||
href="#about"
|
||||
className="text-xl text-white hover:text-blue-400 transition-colors py-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
О нас
|
||||
</Link>
|
||||
<Link
|
||||
href="#contacts"
|
||||
className="text-xl text-white hover:text-blue-400 transition-colors py-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Контакты
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto pb-8">
|
||||
<a
|
||||
href="tel:+78352329226"
|
||||
className="flex items-center text-white hover:text-blue-400 transition-colors mb-2 group"
|
||||
>
|
||||
<Phone className="w-5 h-5 mr-2 group-hover:scale-110 transition-transform" />
|
||||
<span className="text-lg font-semibold">+7 8352 32 92 26</span>
|
||||
</a>
|
||||
<div className="text-sm text-gray-300 mb-6">Пн - Вс с 8:00 до 20:00</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full bg-white text-gray-900 px-6 py-3 rounded-lg hover:bg-gray-100 transition-colors hover:shadow-lg font-semibold"
|
||||
>
|
||||
Оставить заявку
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileMenu;
|
17
src/components/Preloader.tsx
Normal file
17
src/components/Preloader.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
const Preloader = () => (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-white transition-opacity duration-500">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-20 h-20 mb-4 flex items-center justify-center">
|
||||
<svg className="animate-spin-slow" width="64" height="64" viewBox="0 0 64 64" fill="none">
|
||||
<circle cx="32" cy="32" r="28" stroke="#2563eb" strokeWidth="6" strokeDasharray="44 44" strokeLinecap="round" />
|
||||
</svg>
|
||||
<span className="absolute text-3xl font-bold text-blue-600 select-none">В</span>
|
||||
</div>
|
||||
<div className="text-xl font-semibold text-blue-700 tracking-wide animate-pulse">ВашДом</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Preloader;
|
368
src/components/ProjectsSection.tsx
Normal file
368
src/components/ProjectsSection.tsx
Normal file
@ -0,0 +1,368 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useState, useEffect } from 'react';
|
||||
import FadeInSection from './FadeInSection';
|
||||
|
||||
type ProjectsSectionProps = {
|
||||
onCatalogClick?: () => void;
|
||||
};
|
||||
|
||||
const projects = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Гармония',
|
||||
description: 'Дом 11х9м, общая площадь 101,4 кв.м., 3 спальни, 1 санузел',
|
||||
images: [
|
||||
{ type: 'facade', src: '/images/garmony.png', alt: 'Фасад дома Гармония' },
|
||||
{ type: 'plan', src: '/projects/garmony-plan1.jpg', alt: 'Планировка 1 этажа' },
|
||||
{ type: 'plan', src: '/projects/garmony-plan2.jpg', alt: 'Планировка 2 этажа' }
|
||||
],
|
||||
file: '/projects/garmony.pdf',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Горизонт',
|
||||
description: 'Дом 14х13 м, общая площадь 142,8 кв.м, 3 спальни, 2 санузла',
|
||||
images: [
|
||||
{ type: 'facade', src: '/images/gorizont.png', alt: 'Фасад дома Горизонт' },
|
||||
{ type: 'plan', src: '/projects/gorizont-plan1.jpg', alt: 'Планировка 1 этажа' },
|
||||
{ type: 'plan', src: '/projects/gorizont-plan2.jpg', alt: 'Планировка 2 этажа' }
|
||||
],
|
||||
file: '/projects/gorizont.pdf',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Филимонов',
|
||||
description: 'Дом 14,2х10,5 м, общая площадь 131,4 кв.м., 3 спальни, 2 санузла',
|
||||
images: [
|
||||
{ type: 'facade', src: '/images/filimonov.png', alt: 'Фасад дома Филимонов' },
|
||||
{ type: 'plan', src: '/projects/filimonov-plan1.jpg', alt: 'Планировка 1 этажа' },
|
||||
{ type: 'plan', src: '/projects/filimonov-plan2.jpg', alt: 'Планировка 2 этажа' }
|
||||
],
|
||||
file: '/projects/filimonov.pdf',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Моронцов',
|
||||
description: 'Дом 12х8м, общая площадь 89,6 кв.м., 2 спальни, 1 санузел',
|
||||
images: [
|
||||
{ type: 'facade', src: '/images/moronchov.png', alt: 'Фасад дома Моронцов' },
|
||||
{ type: 'plan', src: '/projects/moronchov-plan1.jpg', alt: 'Планировка 1 этажа' },
|
||||
{ type: 'plan', src: '/projects/moronchov-plan2.jpg', alt: 'Планировка 2 этажа' }
|
||||
],
|
||||
file: '/projects/moronchov.pdf',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'Ранчо',
|
||||
description: 'Дом 15х12м, общая площадь 156,8 кв.м., 4 спальни, 2 санузла',
|
||||
images: [
|
||||
{ type: 'facade', src: '/images/rancho.png', alt: 'Фасад дома Ранчо' },
|
||||
{ type: 'plan', src: '/projects/rancho-plan1.jpg', alt: 'Планировка 1 этажа' },
|
||||
{ type: 'plan', src: '/projects/rancho-plan2.jpg', alt: 'Планировка 2 этажа' }
|
||||
],
|
||||
file: '/projects/rancho.pdf',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: 'Тихие Зори',
|
||||
description: 'Дом 13х9м, общая площадь 118,2 кв.м., 3 спальни, 1 санузел',
|
||||
images: [
|
||||
{ type: 'facade', src: '/images/zori.png', alt: 'Фасад дома Тихие Зори' },
|
||||
{ type: 'plan', src: '/projects/zori-plan1.jpg', alt: 'Планировка 1 этажа' },
|
||||
{ type: 'plan', src: '/projects/zori-plan2.jpg', alt: 'Планировка 2 этажа' }
|
||||
],
|
||||
file: '/projects/zori.pdf',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: 'Уютное гнездышко',
|
||||
description: 'Дом 16х10м, общая площадь 168,4 кв.м., 4 спальни, 3 санузла',
|
||||
images: [
|
||||
{ type: 'facade', src: '/images/gnezdo.png', alt: 'Фасад дома Уютное гнездышко' },
|
||||
{ type: 'plan', src: '/projects/gnezdo-plan1.jpg', alt: 'Планировка 1 этажа' },
|
||||
{ type: 'plan', src: '/projects/gnezdo-plan2.jpg', alt: 'Планировка 2 этажа' }
|
||||
],
|
||||
file: '/projects/gnezdo.pdf',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: 'Аура',
|
||||
description: 'Дом 10х8м, общая площадь 78,6 кв.м., 2 спальни, 1 санузел',
|
||||
images: [
|
||||
{ type: 'facade', src: '/images/aura.png', alt: 'Фасад дома Аура' },
|
||||
{ type: 'plan', src: '/projects/aura-plan1.jpg', alt: 'Планировка 1 этажа' },
|
||||
{ type: 'plan', src: '/projects/aura-plan2.jpg', alt: 'Планировка 2 этажа' }
|
||||
],
|
||||
file: '/projects/aura.pdf',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: 'Надежда',
|
||||
description: 'Дом 18х12м, общая площадь 198,4 кв.м., 5 спален, 3 санузла',
|
||||
images: [
|
||||
{ type: 'facade', src: '/images/nade.png', alt: 'Фасад дома Надежда' },
|
||||
{ type: 'plan', src: '/projects/nade-plan1.jpg', alt: 'Планировка 1 этажа' },
|
||||
{ type: 'plan', src: '/projects/nade-plan2.jpg', alt: 'Планировка 2 этажа' }
|
||||
],
|
||||
file: '/projects/nade.pdf',
|
||||
},
|
||||
];
|
||||
|
||||
// Компонент внутреннего слайдера для каждого проекта
|
||||
const ProjectImageSlider = ({ project }: { project: typeof projects[0] }) => {
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
|
||||
const handlePreviousImage = () => {
|
||||
setCurrentImageIndex(prev =>
|
||||
prev === 0 ? project.images.length - 1 : prev - 1
|
||||
);
|
||||
};
|
||||
|
||||
const handleNextImage = () => {
|
||||
setCurrentImageIndex(prev =>
|
||||
prev === project.images.length - 1 ? 0 : prev + 1
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-80 rounded-xl overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-gray-900/80 via-transparent to-transparent z-10"></div>
|
||||
|
||||
{/* Текущее изображение */}
|
||||
<Image
|
||||
src={project.images[currentImageIndex].src}
|
||||
alt={project.images[currentImageIndex].alt}
|
||||
fill
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
|
||||
{/* Навигация по изображениям */}
|
||||
{project.images.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={handlePreviousImage}
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 z-20 bg-white/20 backdrop-blur-sm rounded-full p-2 hover:bg-white/30 transition-all duration-300"
|
||||
aria-label="Предыдущее изображение"
|
||||
>
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleNextImage}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 z-20 bg-white/20 backdrop-blur-sm rounded-full p-2 hover:bg-white/30 transition-all duration-300"
|
||||
aria-label="Следующее изображение"
|
||||
>
|
||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Индикаторы изображений */}
|
||||
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-20 flex space-x-2">
|
||||
{project.images.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentImageIndex(index)}
|
||||
className={`w-2 h-2 rounded-full transition-all duration-300 ${
|
||||
index === currentImageIndex
|
||||
? 'bg-white'
|
||||
: 'bg-white/50 hover:bg-white/75'
|
||||
}`}
|
||||
aria-label={`Изображение ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Тип изображения */}
|
||||
<div className="absolute top-3 left-3 z-20">
|
||||
<span className="px-2 py-1 bg-white/20 backdrop-blur-sm rounded-full text-white text-xs font-medium">
|
||||
{project.images[currentImageIndex].type === 'facade' ? '🏠 Фасад' : '📐 Планировка'}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Декоративные элементы */}
|
||||
<div className="absolute -top-4 -left-4 w-24 h-24 bg-gradient-to-br from-blue-500/30 to-purple-500/30 rounded-full blur-xl"></div>
|
||||
<div className="absolute -bottom-4 -right-4 w-32 h-32 bg-gradient-to-br from-purple-500/30 to-blue-500/30 rounded-full blur-xl"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectsSection = ({ onCatalogClick }: ProjectsSectionProps) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
// Количество проектов для показа на разных экранах
|
||||
const getItemsPerView = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (window.innerWidth >= 1024) return 3; // lg и больше
|
||||
if (window.innerWidth >= 768) return 2; // md
|
||||
return 1; // sm
|
||||
}
|
||||
return 3; // по умолчанию
|
||||
};
|
||||
|
||||
const [itemsPerView, setItemsPerView] = useState(3);
|
||||
|
||||
// Обновляем количество элементов при изменении размера экрана
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setItemsPerView(getItemsPerView());
|
||||
setCurrentIndex(0); // Сбрасываем индекс при изменении размера
|
||||
};
|
||||
|
||||
// Устанавливаем начальное значение
|
||||
setItemsPerView(getItemsPerView());
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// Общее количество слайдов (групп проектов)
|
||||
const totalSlides = Math.ceil(projects.length / itemsPerView);
|
||||
|
||||
const handlePrevious = () => {
|
||||
setCurrentIndex(prev => Math.max(0, prev - 1));
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentIndex(prev => Math.min(totalSlides - 1, prev + 1));
|
||||
};
|
||||
|
||||
// Получаем проекты для текущего слайда
|
||||
const startIndex = currentIndex * itemsPerView;
|
||||
const visibleProjects = projects.slice(startIndex, startIndex + itemsPerView);
|
||||
|
||||
return (
|
||||
<section id="projects" className="relative py-20 bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 overflow-hidden">
|
||||
{/* Статичный фон */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-500 rounded-full blur-3xl"></div>
|
||||
<div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-purple-500 rounded-full blur-3xl"></div>
|
||||
<div className="absolute top-1/2 left-1/2 w-64 h-64 bg-cyan-500 rounded-full blur-3xl opacity-20"></div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
{/* Заголовок секции */}
|
||||
<FadeInSection as="div" className="text-center mb-16">
|
||||
<h2 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-white via-blue-100 to-white bg-clip-text text-transparent mb-4">
|
||||
Каталог проектов
|
||||
</h2>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-blue-500 to-purple-500 mx-auto rounded-full"></div>
|
||||
</FadeInSection>
|
||||
|
||||
{/* Карусель */}
|
||||
<div className="relative">
|
||||
{/* Стрелка влево */}
|
||||
<button
|
||||
onClick={handlePrevious}
|
||||
disabled={currentIndex === 0}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 z-10 bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-md border border-white/20 rounded-full p-3 hover:bg-white/20 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed group"
|
||||
aria-label="Предыдущий проект"
|
||||
>
|
||||
<svg className="w-6 h-6 text-white group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Стрелка вправо */}
|
||||
<button
|
||||
onClick={handleNext}
|
||||
disabled={currentIndex >= totalSlides - 1}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 z-10 bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-md border border-white/20 rounded-full p-3 hover:bg-white/20 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed group"
|
||||
aria-label="Следующий проект"
|
||||
>
|
||||
<svg className="w-6 h-6 text-white group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Контейнер проектов */}
|
||||
<div className="overflow-hidden">
|
||||
<div
|
||||
className="grid transition-transform duration-300 ease-in-out gap-8"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${itemsPerView}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{visibleProjects.map((project, index) => (
|
||||
<FadeInSection
|
||||
key={`${project.id}-${currentIndex}`}
|
||||
as="div"
|
||||
className="group relative p-6 rounded-2xl bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-md border border-white/20 hover:border-white/40 transition-all duration-500 hover:scale-105 hover:-translate-y-2"
|
||||
delay={0.1 * index}
|
||||
>
|
||||
{/* Фон */}
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-blue-500/20 to-purple-500/20 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
{/* Слайдер изображений */}
|
||||
<ProjectImageSlider project={project} />
|
||||
|
||||
{/* Информация о проекте */}
|
||||
<div className="mt-6">
|
||||
<h3 className="text-2xl font-bold text-white mb-3 group-hover:text-blue-300 transition-all duration-300">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="text-gray-300 leading-relaxed mb-4">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
{/* Дополнительная информация */}
|
||||
<div className="flex items-center justify-between text-sm text-gray-400">
|
||||
<span className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
|
||||
<span>Доступен для строительства</span>
|
||||
</span>
|
||||
<span className="flex items-center space-x-1">
|
||||
<span>📐</span>
|
||||
<span>{project.images.length - 1} планировки</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Декоративный элемент */}
|
||||
<div className="absolute top-2 right-2 w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full opacity-50 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
</div>
|
||||
</FadeInSection>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Индикаторы */}
|
||||
<div className="flex justify-center mt-12 space-x-3">
|
||||
{Array.from({ length: totalSlides }, (_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentIndex(index)}
|
||||
className={`w-3 h-3 rounded-full transition-all duration-300 ${
|
||||
index === currentIndex
|
||||
? 'bg-gradient-to-r from-blue-500 to-purple-500 scale-125'
|
||||
: 'bg-white/30 hover:bg-white/50'
|
||||
}`}
|
||||
aria-label={`Перейти к слайду ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Призыв к действию */}
|
||||
<FadeInSection as="div" delay={0.6} className="text-center mt-16">
|
||||
<button
|
||||
onClick={onCatalogClick}
|
||||
className="inline-flex items-center space-x-2 px-8 py-4 rounded-full bg-gradient-to-r from-blue-500/20 to-purple-500/20 border border-white/20 backdrop-blur-sm hover:scale-105 transition-transform duration-300 cursor-pointer group"
|
||||
>
|
||||
<span className="text-white font-medium text-lg">Получить полный каталог проектов</span>
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full group-hover:scale-150 transition-transform duration-300"></div>
|
||||
</button>
|
||||
</FadeInSection>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectsSection;
|
92
src/components/ReviewsSection.tsx
Normal file
92
src/components/ReviewsSection.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import Image from 'next/image';
|
||||
import { Star } from 'lucide-react';
|
||||
import FadeInSection from './FadeInSection';
|
||||
|
||||
type ReviewsSectionProps = {
|
||||
onExcursionClick?: () => void;
|
||||
};
|
||||
|
||||
const reviews = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Александр Петров',
|
||||
text: 'Очень доволен качеством строительства. Команда профессионалов, все работы выполнены в срок и с соблюдением всех норм.',
|
||||
rating: 5,
|
||||
image: '/images/Sasha.jpg',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Елена Смирнова',
|
||||
text: 'Спасибо за отличную работу! Дом построен качественно, все пожелания были учтены. Рекомендую всем!',
|
||||
rating: 5,
|
||||
image: '/images/Elena.jpg',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Дмитрий Иванов',
|
||||
text: 'Профессиональный подход к делу. Все этапы строительства контролировались, результат превзошел ожидания.',
|
||||
rating: 5,
|
||||
image: '/images/Dmitry.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
const ReviewsSection = ({ onExcursionClick }: ReviewsSectionProps) => {
|
||||
return (
|
||||
<section id="reviews" className="py-20 bg-gray-50">
|
||||
<div className="container mx-auto px-4">
|
||||
<FadeInSection as="h2" className="text-3xl font-bold text-gray-900 mb-12 text-center">
|
||||
Отзывы клиентов
|
||||
</FadeInSection>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{reviews.map((review, index) => (
|
||||
<FadeInSection
|
||||
key={review.id}
|
||||
as="div"
|
||||
className="bg-white p-6 rounded-lg shadow-sm"
|
||||
delay={0.2 * index}
|
||||
>
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="relative w-12 h-12 rounded-full overflow-hidden mr-4">
|
||||
<Image
|
||||
src={review.image}
|
||||
alt={review.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
{review.name}
|
||||
</h3>
|
||||
<div className="flex">
|
||||
{[...Array(review.rating)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className="w-4 h-4 text-yellow-400 fill-current"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600">
|
||||
{review.text}
|
||||
</p>
|
||||
</FadeInSection>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<FadeInSection as="div" className="mt-12 text-center" delay={0.8}>
|
||||
<button
|
||||
className="bg-white border-2 border-blue-600 text-blue-600 px-8 py-3 rounded-full hover:bg-blue-50 transition-colors"
|
||||
onClick={onExcursionClick}
|
||||
>
|
||||
Записаться на экскурсию
|
||||
</button>
|
||||
</FadeInSection>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReviewsSection;
|
73
src/components/TeamSection.tsx
Normal file
73
src/components/TeamSection.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import Image from 'next/image';
|
||||
import FadeInSection from './FadeInSection';
|
||||
|
||||
const team = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Степанов Денис',
|
||||
position: 'Основатель и владелец компании',
|
||||
image: '/images/Stepan.jpg',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Романов Даниил',
|
||||
position: 'Генеральный директор',
|
||||
image: '/images/Roman.jpg',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Степанова Оксана',
|
||||
position: 'Финансовый директор',
|
||||
image: '/images/Oksana.jpg',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Семенов Максим',
|
||||
position: 'Производитель работ',
|
||||
image: '/images/Maksim.jpg',
|
||||
},
|
||||
];
|
||||
|
||||
const TeamSection = () => {
|
||||
return (
|
||||
<section className="py-16 md:py-20 bg-white">
|
||||
<div className="container mx-auto px-4">
|
||||
<FadeInSection as="h2" className="text-2xl sm:text-3xl font-bold text-gray-900 mb-6 md:mb-12 text-center">
|
||||
Наша команда
|
||||
</FadeInSection>
|
||||
<FadeInSection as="p" className="text-base sm:text-lg text-gray-600 text-center mb-8 md:mb-12 max-w-2xl mx-auto" delay={0.2}>
|
||||
Каждый день работает над тем, чтобы предоставить лучший сервис и сделать наших клиентов счастливыми
|
||||
</FadeInSection>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 md:gap-8">
|
||||
{team.map((member, index) => (
|
||||
<FadeInSection
|
||||
key={member.id}
|
||||
as="div"
|
||||
className="text-center bg-gray-50 p-4 rounded-xl hover:shadow-lg transition-shadow"
|
||||
delay={0.3 + (index * 0.1)}
|
||||
>
|
||||
<div className="relative w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 mx-auto mb-4 rounded-full overflow-hidden">
|
||||
<Image
|
||||
src={member.image}
|
||||
alt={member.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw"
|
||||
/>
|
||||
</div>
|
||||
<h3 className="text-lg sm:text-xl font-bold text-gray-900 mb-2">
|
||||
{member.name}
|
||||
</h3>
|
||||
<p className="text-sm sm:text-base text-gray-600">
|
||||
{member.position}
|
||||
</p>
|
||||
</FadeInSection>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamSection;
|
58
src/components/WhyChooseUsSection.tsx
Normal file
58
src/components/WhyChooseUsSection.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Shield, FileText, Clock, Users } from 'lucide-react';
|
||||
import FadeInSection from './FadeInSection';
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: Shield,
|
||||
title: 'Аккредитованный застройщик',
|
||||
description: 'Ведущими банками для оформления безопасной и выгодной сделки',
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
title: 'Открытые и честные сметы',
|
||||
description: 'Закрытый договор с фиксацией цены, стоимость не изменится на протяжении всего строительства',
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
title: 'Повышенная гарантия до 5 лет',
|
||||
description: 'Гарантия качества дома',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'Профессиональная команда',
|
||||
description: 'Опытные специалисты с многолетним стажем в строительстве',
|
||||
},
|
||||
];
|
||||
|
||||
const WhyChooseUsSection = () => {
|
||||
return (
|
||||
<section className="py-20 bg-gray-50">
|
||||
<div className="container mx-auto px-4">
|
||||
<FadeInSection as="h2" className="text-3xl font-bold text-gray-900 mb-12 text-center">
|
||||
Почему выбирают нас?
|
||||
</FadeInSection>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{features.map((feature, index) => (
|
||||
<FadeInSection
|
||||
key={index}
|
||||
as="div"
|
||||
className="bg-white p-6 rounded-lg shadow-sm"
|
||||
delay={0.2 * index}
|
||||
>
|
||||
<feature.icon className="w-12 h-12 text-blue-600 mb-4" />
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
{feature.description}
|
||||
</p>
|
||||
</FadeInSection>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhyChooseUsSection;
|
Reference in New Issue
Block a user