const fs = require('fs'); const path = require('path'); // Это СТРУКТУРА проекта Next.js 14, которую нужно создать // Всё ниже — готовый код для копирования в файлы const PROJECT_STRUCTURE = { 'next.config.js': `/** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, compress: true, poweredByHeader: false, }; module.exports = nextConfig; `, 'tailwind.config.ts': `import type { Config } from 'tailwindcss' const config: Config = { content: [ './app/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { primary: '#1a2847', accent: '#ff8c00', light: '#f5f7fa', }, fontFamily: { sans: ['-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'sans-serif'], }, }, }, plugins: [], } export default config `, 'app/layout.tsx': `import type { Metadata } from 'next' import './globals.css' export const metadata: Metadata = { title: 'Откачка септиков и канализации в Иркутске — заказать ассенизаторскую машину', description: 'Откачка септиков, выгребных ям и канализации в Иркутске. Машины разного объёма. Расчёт стоимости онлайн. Быстрый выезд.', keywords: 'откачка септика, канализация, выгребная яма, Иркутск', openGraph: { title: 'Откачка септиков в Иркутске', description: 'Быстрая откачка септиков. Расчёт онлайн за 30 секунд.', type: 'website', }, } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) } `, 'app/globals.css': `@tailwind base; @tailwind components; @tailwind utilities; * { margin: 0; padding: 0; box-sizing: border-box; } html { scroll-behavior: smooth; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } button { font-family: inherit; } input, textarea, select { font-family: inherit; font-size: 16px; } @media (max-width: 640px) { input, textarea, select { font-size: 16px; /* Prevent zoom on iOS */ } } .sticky-cta { position: fixed; bottom: 0; left: 0; right: 0; background: white; border-top: 1px solid #e5e7eb; padding: 1rem; z-index: 40; } .container-safe { max-width: 1200px; margin: 0 auto; padding: 0 1rem; } `, 'lib/config.ts': `export const VEHICLES = [ { id: 'vehicle-3', name: 'Машина 3 м³', tankVolume: 3, hoseLength: 20, basePrice: 3000, pricePerKm: 100, available: true, image: '🚛', }, { id: 'vehicle-5', name: 'Машина 5 м³', tankVolume: 5, hoseLength: 20, basePrice: 3500, pricePerKm: 100, available: true, image: '🚛', }, { id: 'vehicle-8', name: 'Машина 8 м³', tankVolume: 8, hoseLength: 30, basePrice: 4000, pricePerKm: 100, available: true, image: '🚛', }, { id: 'vehicle-10', name: 'Машина 10 м³', tankVolume: 10, hoseLength: 30, basePrice: 4500, pricePerKm: 100, available: true, image: '🚛', }, { id: 'vehicle-15', name: 'Машина 15 м³', tankVolume: 15, hoseLength: 40, basePrice: 5000, pricePerKm: 100, available: true, image: '🚛', }, ]; export const PRICING = { baseCalloutPrice: 3000, pricePerKm: 100, disposalPricePerM3: 500, urgentMultiplier: 1.3, nightMultiplier: 1.5, minimumOrderPrice: 3500, }; export const DISPOSAL_POINTS = [ { id: 'dp-1', name: 'Иркутский полигон ТБО', address: 'Иркутск, Академгородок', latitude: 52.32, longitude: 104.28, workingHours: '08:00-18:00', acceptedWasteTypes: ['liquid', 'sludge'], pricePerM3: 500, active: true, }, { id: 'dp-2', name: 'Центр утилизации "Чистая вода"', address: 'Иркутск, Юбилейный микрорайон', latitude: 52.29, longitude: 104.31, workingHours: '08:00-20:00', acceptedWasteTypes: ['liquid', 'sludge'], pricePerM3: 520, active: true, }, { id: 'dp-3', name: 'Завод по переработке отходов', address: 'Иркутск, Промышленная зона', latitude: 52.35, longitude: 104.25, workingHours: '06:00-22:00', acceptedWasteTypes: ['liquid', 'sludge', 'industrial'], pricePerM3: 480, active: true, }, ]; export const SERVICE_AREAS = [ { name: 'Иркутск центр', centerLat: 52.3, centerLon: 104.3, radiusKm: 10, multiplier: 1.0, }, { name: 'Иркутск пригород', centerLat: 52.3, centerLon: 104.3, radiusKm: 30, multiplier: 1.1, }, { name: 'Иркутская область', centerLat: 52.3, centerLon: 104.3, radiusKm: 80, multiplier: 1.2, }, ]; export const ADDITIONAL_SERVICES = [ { id: 'urgent', name: 'Срочный выезд', type: 'multiplier', value: 1.3 }, { id: 'night', name: 'Ночной выезд (20:00-08:00)', type: 'multiplier', value: 1.5 }, { id: 'long-hose', name: 'Увеличенная длина рукава', type: 'fixed', value: 1000 }, { id: 'sewer-cleaning', name: 'Прочистка канализации', type: 'fixed', value: 3000 }, ]; `, 'lib/types.ts': `export interface Vehicle { id: string; name: string; tankVolume: number; hoseLength: number; basePrice: number; pricePerKm: number; available: boolean; image?: string; } export interface DisposalPoint { id: string; name: string; address: string; latitude: number; longitude: number; workingHours: string; acceptedWasteTypes: string[]; pricePerM3: number; active: boolean; } export interface Coordinates { latitude: number; longitude: number; } export interface CalculationResult { vehicle: Vehicle; requestedVolume: number; trips: number; distanceKm: number; disposalPoint: DisposalPoint; basePrice: number; transportPrice: number; disposalPrice: number; subtotal: number; total: number; estimatedDuration: number; address: string; additionalServices?: string[]; } export interface Order { id: string; customerName: string; phone: string; address: string; coordinates: Coordinates; requestedVolume: number; vehicleId: string; trips: number; disposalPointId: string; estimatedPrice: number; comment?: string; status: 'NEW' | 'CONFIRMED' | 'ASSIGNED' | 'EN_ROUTE' | 'COMPLETED' | 'CANCELLED'; createdAt: string; } `, 'lib/calculations.ts': `import { VEHICLES, PRICING, DISPOSAL_POINTS, SERVICE_AREAS } from './config'; import { Vehicle, DisposalPoint, CalculationResult } from './types'; export function findBestVehicle(requestedVolume: number): Vehicle | null { const suitable = VEHICLES.filter(v => v.tankVolume >= requestedVolume && v.available); return suitable.length > 0 ? suitable[0] : null; } export function calculateTrips(requestedVolume: number, vehicle: Vehicle): number { return Math.ceil(requestedVolume / vehicle.tankVolume); } export function calculateDistance(): number { // Mock: случайное расстояние от 5 до 80 км return Math.floor(Math.random() * 75) + 5; } export function calculateEstimatedTime(distanceKm: number): number { // Примерно 1.5 км/мин в городе return Math.round(distanceKm / 1.5) + 10; } export function selectOptimalDisposalPoint( distanceKm: number, disposalPoints: DisposalPoint[] = DISPOSAL_POINTS ): DisposalPoint { // Mock: выбираем рандомную точку const active = disposalPoints.filter(dp => dp.active); return active[Math.floor(Math.random() * active.length)]; } export function calculatePrice( volume: number, distanceKm: number, vehicle: Vehicle, disposalPoint: DisposalPoint, additionalServices: string[] = [] ): CalculationResult { const trips = calculateTrips(volume, vehicle); const basePrice = vehicle.basePrice; const transportPrice = vehicle.pricePerKm * distanceKm * trips; const disposalPrice = volume * disposalPoint.pricePerM3; let subtotal = basePrice + transportPrice + disposalPrice; // Дополнительные услуги let additionalServicesPrice = 0; let multiplier = 1; for (const serviceId of additionalServices) { const service = require('./config').ADDITIONAL_SERVICES.find( (s: any) => s.id === serviceId ); if (service) { if (service.type === 'fixed') { additionalServicesPrice += service.value; } else if (service.type === 'multiplier') { multiplier *= service.value; } } } subtotal += additionalServicesPrice; let total = Math.round(subtotal * multiplier); // Минимальная стоимость if (total < PRICING.minimumOrderPrice) { total = PRICING.minimumOrderPrice; } return { vehicle, requestedVolume: volume, trips, distanceKm, disposalPoint, basePrice, transportPrice, disposalPrice, subtotal, total, estimatedDuration: calculateEstimatedTime(distanceKm), address: 'Иркутск', additionalServices, }; } export function hasCoverage(latitude: number, longitude: number): boolean { // Простая проверка: всё в пределах Иркутской области (mock) // В реальности нужно проверить против SERVICE_AREAS return Math.abs(latitude - 52.3) < 2 && Math.abs(longitude - 104.3) < 2; } `, 'app/page.tsx': `'use client'; import { useState } from 'react'; import Hero from '@/components/Hero'; import Calculator from '@/components/Calculator'; import Services from '@/components/Services'; import Fleet from '@/components/Fleet'; import Trust from '@/components/Trust'; import FAQ from '@/components/FAQ'; import Footer from '@/components/Footer'; export default function Home() { return (
); } `, 'components/Hero.tsx': `export default function Hero() { return (

Откачка септиков и канализации в Иркутске

Подберём машину по объёму, рассчитаем примерную стоимость и найдём ближайшую точку утилизации.

✓ Выезд по Иркутску и ближайшим районам
✓ Машины разного объёма
✓ Быстрый расчёт стоимости
✓ Работаем с частными домами и организациями

📞 +7 (902) 555-12-34

); } `, 'components/Calculator.tsx': `'use client'; import { useState, useRef, useEffect } from 'react'; import VolumeSelector from './VolumeSelector'; import ResultCard from './ResultCard'; import OrderForm from './OrderForm'; import { findBestVehicle, calculatePrice, calculateDistance } from '@/lib/calculations'; import { DISPOSAL_POINTS, VEHICLES } from '@/lib/config'; import { CalculationResult } from '@/lib/types'; export default function Calculator() { const [step, setStep] = useState<'volume' | 'address' | 'result'>('volume'); const [volume, setVolume] = useState(null); const [address, setAddress] = useState(''); const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [showForm, setShowForm] = useState(false); const resultRef = useRef(null); const handleVolumeSelect = (vol: number) => { setVolume(vol); setStep('address'); }; const handleAddressSubmit = async (addr: string) => { setAddress(addr); setLoading(true); setError(''); try { // Мок геокодирование const response = await fetch('/api/geocode', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr }), }); if (!response.ok) throw new Error('Адрес не найден'); const geo = await response.json(); // Расчёт стоимости const calcResponse = await fetch('/api/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr, volume: volume, coordinates: geo, }), }); const calc = await calcResponse.json(); setResult(calc); setStep('result'); // Скролл к результату setTimeout(() => { resultRef.current?.scrollIntoView({ behavior: 'smooth' }); }, 100); } catch (err) { setError(err instanceof Error ? err.message : 'Ошибка расчёта'); } finally { setLoading(false); } }; return (

Рассчитайте стоимость откачки

{step === 'volume' && ( )} {step === 'address' && (

Введите адрес

setAddress(e.target.value)} className="w-full px-4 py-3 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-accent" disabled={loading} /> {error && (
{error}
)}
)} {step === 'result' && result && (
setShowForm(true)} /> {showForm && (
setShowForm(false)} />
)}
)}
); } `, 'components/VolumeSelector.tsx': `'use client'; import { useState } from 'react'; interface Props { onSelect: (volume: number) => void; } export default function VolumeSelector({ onSelect }: Props) { const [mode, setMode] = useState<'simple' | 'pro'>('simple'); const simpleOptions = [ { label: 'До 3 м³', value: 3 }, { label: '3–5 м³', value: 5 }, { label: '5–8 м³', value: 8 }, { label: '8–10 м³', value: 10 }, { label: 'Более 10 м³', value: 15 }, ]; const proOptions = [ { label: '3 м³', value: 3 }, { label: '5 м³', value: 5 }, { label: '8 м³', value: 8 }, { label: '10 м³', value: 10 }, { label: '12 м³', value: 12 }, { label: '15 м³', value: 15 }, { label: '20 м³', value: 20 }, ]; const options = mode === 'simple' ? simpleOptions : proOptions; return (

Выберите примерный объём

{options.map((opt) => ( ))}
); } `, 'components/ResultCard.tsx': `'use client'; import { CalculationResult } from '@/lib/types'; interface Props { calculation: CalculationResult; onOrderClick: () => void; } export default function ResultCard({ calculation, onOrderClick }: Props) { const { vehicle, trips, distanceKm, disposalPoint, total, estimatedDuration, address } = calculation; return (

Предварительный расчёт

📍 Адрес: {address}
🚛 Машина: {vehicle.name}
{trips > 1 && (
🔄 Рейсов: {trips}
)}
♻️ Точка утилизации: {disposalPoint.name}
📏 Расстояние: {distanceKm.toFixed(1)} км
⏱️ Ориентировочное время: ~{estimatedDuration} минут

Примерная стоимость:

от {total.toLocaleString()} ₽

Точная стоимость зависит от фактического объёма, расстояния, условий подъезда и действующих тарифов. Окончательную стоимость подтвердит оператор.

); } `, 'components/OrderForm.tsx': `'use client'; import { useState } from 'react'; import { CalculationResult } from '@/lib/types'; interface Props { calculation: CalculationResult; onClose: () => void; } export default function OrderForm({ calculation, onClose }: Props) { const [formData, setFormData] = useState({ name: '', phone: '', date: '', time: '', comment: '', privacy: false, }); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const handleChange = (e: React.ChangeEvent) => { const { name, value, type } = e.target; setFormData({ ...formData, [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value, }); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); try { const response = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...formData, calculation, }), }); if (!response.ok) throw new Error('Ошибка отправки заявки'); setSuccess(true); setTimeout(() => { onClose(); }, 2000); } catch (err) { alert('Ошибка: ' + (err instanceof Error ? err.message : 'неизвестная ошибка')); } finally { setLoading(false); } }; if (success) { return (

✓ Заявка принята!

Оператор свяжется с вами для подтверждения заказа и уточнения деталей.

Спасибо за доверие!

); } return (

Оставить заявку