Initial commit: YourHouse project

This commit is contained in:
Bivekich
2025-06-26 05:37:29 +03:00
commit 827e8d5ab2
73 changed files with 9365 additions and 0 deletions

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

151
src/app/globals.css Normal file
View 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
View 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
View 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
View 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">
Нажимая кнопку &quot;Обсудить проект&quot;, вы соглашаетесь с{' '}
<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>
);
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;