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 (
Откачка септиков и канализации в Иркутске
Подберём машину по объёму, рассчитаем примерную стоимость и найдём ближайшую точку утилизации.
✓ Выезд по Иркутску и ближайшим районам
✓ Машины разного объёма
✓ Быстрый расчёт стоимости
✓ Работаем с частными домами и организациями
);
}
`,
'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 (
);
}
`,
'components/Services.tsx': `export default function Services() {
const services = [
{ icon: '💧', title: 'Откачка септика', desc: 'Быстрая и качественная откачка жидких бытовых отходов' },
{ icon: '🔧', title: 'Прочистка канализации', desc: 'Устранение засоров и восстановление проходимости' },
{ icon: '🚜', title: 'Откачка ямы', desc: 'Выгребные ямы, картеры, резервуары любого объёма' },
{ icon: '♻️', title: 'Официальная утилизация', desc: 'Вывоз на лицензированные точки утилизации' },
{ icon: '🛠️', title: 'Обслуживание', desc: 'Регулярное обслуживание и профилактика' },
{ icon: '⚡', title: 'Срочный выезд', desc: 'Экстренные вызовы в течение 1–2 часов' },
];
return (
Сопутствующие услуги
{services.map((service, i) => (
{service.icon}
{service.title}
{service.desc}
))}
);
}
`,
'components/Trust.tsx': `export default function Trust() {
const reasons = [
'✓ Собственный автопарк',
'✓ Работаем по Иркутску и области',
'✓ Прозрачный расчёт',
'✓ Работаем без посредников',
'✓ Возможность срочного выезда',
'✓ Официальная утилизация отходов',
];
return (
Почему нас выбирают
{reasons.map((reason, i) => (
{reason}
))}
);
}
`,
'components/Fleet.tsx': `export default function Fleet() {
const vehicles = [
{ volume: '3 м³', hose: '20 м', desc: 'Для небольших объёмов и тесных участков' },
{ volume: '5 м³', hose: '20 м', desc: 'Оптимальный вариант для частных домов' },
{ volume: '8 м³', hose: '30 м', desc: 'Для средних и больших септиков' },
{ volume: '10 м³', hose: '30 м', desc: 'Универсальная машина для большинства задач' },
{ volume: '15 м³', hose: '40 м', desc: 'Для промышленных объёмов и производств' },
];
return (
Наш автопарк
{vehicles.map((vehicle, i) => (
🚛
Машина {vehicle.volume}
Объём: {vehicle.volume}
Рукав: {vehicle.hose}
{vehicle.desc}
))}
);
}
`,
'components/FAQ.tsx': `'use client';
import { useState } from 'react';
export default function FAQ() {
const [open, setOpen] = useState(null);
const faqs = [
{
q: 'Как быстро приедет машина?',
a: 'В стандартном режиме машина приезжает в течение 24 часов. При срочном заказе — за 1–2 часа.',
},
{
q: 'Какой объём машины выбрать?',
a: 'Используйте наш калькулятор. Введите примерный объём вашего септика — система подберёт подходящую машину.',
},
{
q: 'Сколько стоит откачка септика?',
a: 'Цена зависит от объёма, расстояния и других факторов. Используйте калькулятор для предварительного расчёта.',
},
{
q: 'Можно ли заказать машину ночью?',
a: 'Да, круглосуточно. Ночной выезд стоит дороже на 50% (коэффициент 1.5).',
},
{
q: 'Как рассчитывается стоимость?',
a: 'Стоимость = подача + пробег + утилизация + дополнительные услуги. Точную сумму подтвердит оператор.',
},
{
q: 'Куда вывозятся отходы?',
a: 'На лицензированные точки утилизации в соответствии с законодательством.',
},
];
return (
Часто задаваемые вопросы
{faqs.map((faq, i) => (
{open === i && (
{faq.a}
)}
))}
);
}
`,
'components/Footer.tsx': `export default function Footer() {
return (
);
}
`,
'app/api/geocode/route.ts': `import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const { address } = await request.json();
if (!address) {
return NextResponse.json(
{ error: 'Адрес не указан' },
{ status: 400 }
);
}
// Mock геокодирование
// В реальности здесь был бы вызов Яндекс.Карт или другого сервиса
const mockCoordinates = {
latitude: 52.3 + (Math.random() - 0.5) * 0.5,
longitude: 104.3 + (Math.random() - 0.5) * 0.5,
address: address,
};
return NextResponse.json(mockCoordinates);
} catch (error) {
return NextResponse.json(
{ error: 'Ошибка геокодирования' },
{ status: 500 }
);
}
}
`,
'app/api/calculate/route.ts': `import { NextRequest, NextResponse } from 'next/server';
import { findBestVehicle, calculatePrice, calculateDistance, selectOptimalDisposalPoint } from '@/lib/calculations';
export async function POST(request: NextRequest) {
try {
const { address, volume, coordinates } = await request.json();
if (!volume || !address) {
return NextResponse.json(
{ error: 'Недостаточно данных' },
{ status: 400 }
);
}
// Поиск подходящей машины
const vehicle = findBestVehicle(volume);
if (!vehicle) {
return NextResponse.json(
{ error: 'Нет подходящей машины' },
{ status: 400 }
);
}
// Выбор точки сброса
const disposalPoint = selectOptimalDisposalPoint(0);
// Расчёт расстояния (mock)
const distanceKm = calculateDistance();
// Расчёт стоимости
const calculation = calculatePrice(volume, distanceKm, vehicle, disposalPoint);
return NextResponse.json({
...calculation,
address,
});
} catch (error) {
return NextResponse.json(
{ error: 'Ошибка расчёта' },
{ status: 500 }
);
}
}
`,
'app/api/disposal-points/route.ts': `import { NextRequest, NextResponse } from 'next/server';
import { DISPOSAL_POINTS } from '@/lib/config';
export async function GET(request: NextRequest) {
try {
const active = DISPOSAL_POINTS.filter(dp => dp.active);
return NextResponse.json(active);
} catch (error) {
return NextResponse.json(
{ error: 'Ошибка получения точек' },
{ status: 500 }
);
}
}
`,
'app/api/vehicles/route.ts': `import { NextRequest, NextResponse } from 'next/server';
import { VEHICLES } from '@/lib/config';
export async function GET(request: NextRequest) {
try {
const available = VEHICLES.filter(v => v.available);
return NextResponse.json(available);
} catch (error) {
return NextResponse.json(
{ error: 'Ошибка получения автопарка' },
{ status: 500 }
);
}
}
`,
'app/api/orders/route.ts': `import { NextRequest, NextResponse } from 'next/server';
// Mock database (в реальности это была бы БД)
const orders: any[] = [];
export async function POST(request: NextRequest) {
try {
const data = await request.json();
const order = {
id: Date.now().toString(),
...data,
createdAt: new Date().toISOString(),
status: 'NEW',
};
orders.push(order);
console.log('New order:', order);
return NextResponse.json({
success: true,
orderId: order.id,
message: 'Заявка принята',
});
} catch (error) {
return NextResponse.json(
{ error: 'Ошибка сохранения заявки' },
{ status: 500 }
);
}
}
export async function GET(request: NextRequest) {
return NextResponse.json({
orders,
total: orders.length,
});
}
`,
'package.json': `{
"name": "septic-service",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"next": "^14.0.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"tailwindcss": "^3.3.0",
"postcss": "^8.4.0",
"autoprefixer": "^10.4.0"
}
}
`,
'postcss.config.js': `module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
`,
'.env.local': `# Demo API keys (не использовать в production)
NEXT_PUBLIC_API_URL=http://localhost:3000
NEXT_PUBLIC_DEMO_MODE=true
`,
'tsconfig.json': `{
"compilerOptions": {
"target": "es2020",
"lib": ["es2020", "dom", "dom.iterable"],
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
`,
};
// Вывод структуры для создания проекта
console.log('📁 СТРУКТУРА ПРОЕКТА NEXT.JS:');
console.log('\n' + Object.keys(PROJECT_STRUCTURE).join('\n'));
console.log('\n✅ Используйте эту структуру для создания проекта.');