Загрузка данных
<div class="landing">
<header class="header">
<button class="brand" type="button" (click)="scrollToSection('top')" aria-label="В начало">
EstateValue
</button>
<button class="menu-toggle" type="button" (click)="toggleMobileMenu()" aria-label="Открыть меню">
<span></span>
<span></span>
<span></span>
</button>
<nav class="nav" [class.nav_open]="isMobileMenuOpen" aria-label="Навигация по секциям">
<button type="button" (click)="scrollToSection('benefits')">Преимущества</button>
<button type="button" (click)="scrollToSection('pricing')">Тарифы</button>
<button type="button" (click)="scrollToSection('lead-form')">Заявка</button>
<button class="button button_secondary" type="button" (click)="scrollToSection('lead-form')">
Оставить заявку
</button>
</nav>
</header>
<main id="top">
<section class="hero">
<div class="hero__content">
<p class="hero__badge">Роль: Гость</p>
<h1>Оценка имущества без визита в офис</h1>
<p class="hero__description">
Оставьте заявку, выберите тариф и получите консультацию в тот же день.
</p>
<div class="hero__chips">
<span>До 30 мин на ответ</span>
<span>Прозрачные тарифы</span>
<span>Поддержка 7 дней</span>
</div>
<div class="hero__actions">
<button class="button" type="button" (click)="scrollToSection('lead-form')">Начать сейчас</button>
<button class="button button_secondary" type="button" (click)="scrollToSection('pricing')">
Смотреть тарифы
</button>
</div>
</div>
<aside class="hero__card" aria-label="Ключевые показатели">
<p class="hero__card-title">Коротко о главном</p>
<ul>
<li>Ответ менеджера до 30 минут</li>
<li>Прозрачная цена без скрытых платежей</li>
<li>Шаблоны под нотариальные требования</li>
</ul>
</aside>
</section>
<section id="benefits" class="section">
<h2>Преимущества сервиса</h2>
<div class="grid grid_3">
@for (benefit of benefits; track benefit.title) {
<article class="card">
<h3>{{ benefit.title }}</h3>
<p>{{ benefit.description }}</p>
</article>
}
</div>
</section>
<section class="section section_muted">
<h2>Как это работает</h2>
<div class="steps">
@for (step of steps; track step.title; let idx = $index) {
<article class="step">
<span class="step__index">0{{ idx + 1 }}</span>
<h3>{{ step.title }}</h3>
<p>{{ step.description }}</p>
</article>
}
</div>
</section>
<section id="pricing" class="section">
<div class="section__head">
<h2>Тарифы</h2>
<p>Выбранный тариф: <strong>{{ selectedPlanTitle }}</strong></p>
</div>
<div class="grid grid_3">
@for (plan of plans; track plan.title) {
<article class="card card_pricing" [class.card_active]="selectedPlanTitle === plan.title">
<h3>{{ plan.title }}</h3>
<p class="price">{{ plan.price }} ₽</p>
<p class="period">{{ plan.period }}</p>
<ul>
@for (feature of plan.features; track feature) {
<li>{{ feature }}</li>
}
</ul>
<button class="button" type="button" (click)="selectPlan(plan.title)">{{ plan.cta }}</button>
</article>
}
</div>
</section>
<section id="lead-form" class="section section_muted">
<h2>Оставьте заявку</h2>
<p>Заполнив форму, вы получите звонок и персональный расчет.</p>
<form class="lead-form" #leadFormRef>
<label>
Имя
<input #nameInput type="text" name="name" placeholder="Ваше имя" />
</label>
<label>
Телефон
<input #phoneInput type="tel" name="phone" placeholder="+7 (___) ___-__-__" />
</label>
<button class="button" type="button" (click)="submitLead(nameInput.value, phoneInput.value)">
Отправить заявку
</button>
</form>
@if (leadStatus) {
<p class="status" role="status">{{ leadStatus }}</p>
}
</section>
<section id="faq" class="section">
<h2>FAQ</h2>
<div class="faq">
@for (item of faq; track item.question; let idx = $index) {
<article class="faq__item">
<button class="faq__question" type="button" (click)="toggleFaq(idx)">
<span>{{ item.question }}</span>
<span class="faq__icon">{{ isFaqOpen(idx) ? '−' : '+' }}</span>
</button>
@if (isFaqOpen(idx)) {
<p class="faq__answer">{{ item.answer }}</p>
}
</article>
}
</div>
</section>
</main>
</div>
___________________________________CSS__________________________________
:host {
display: block;
background:
radial-gradient(circle at 0% 0%, #eaf0ff 0%, transparent 38%),
radial-gradient(circle at 100% 10%, #efe7ff 0%, transparent 34%),
linear-gradient(180deg, #f7f9ff 0%, #ffffff 360px);
color: #17213a;
font-family: Inter, Arial, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
.landing {
max-width: 1080px;
margin: 0 auto;
padding: 18px 20px 46px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 8px 0 16px;
}
.brand {
border: 0;
background: transparent;
color: #1a2854;
font-size: 1.2rem;
font-weight: 800;
letter-spacing: 0.01em;
cursor: pointer;
}
.menu-toggle {
display: none;
border: 0;
background: transparent;
cursor: pointer;
padding: 6px;
}
.menu-toggle span {
display: block;
width: 22px;
height: 2px;
background: #18223a;
margin: 4px 0;
}
.nav {
display: flex;
align-items: center;
gap: 14px;
}
.nav button {
border: 0;
background: transparent;
color: #2c3b66;
font: inherit;
font-weight: 500;
cursor: pointer;
border-radius: 10px;
padding: 8px 10px;
transition: background 0.2s ease;
}
.nav button:hover {
background: #e9efff;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #3f61ff;
border-radius: 10px;
background: linear-gradient(135deg, #4a6bff 0%, #3654de 100%);
color: #fff;
text-decoration: none;
padding: 10px 16px;
font-weight: 600;
cursor: pointer;
transition: 0.2s ease;
}
.button:hover {
transform: translateY(-1px);
box-shadow: 0 10px 18px rgb(53 84 222 / 24%);
}
.button_secondary {
background: #fff;
color: #3454d6;
border-color: #d3ddff;
}
.button_secondary:hover {
background: #f3f6ff;
box-shadow: none;
}
.hero {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 18px;
align-items: stretch;
margin-bottom: 10px;
}
.hero__content,
.hero__card {
background: #ffffff;
border: 1px solid #e0e8ff;
border-radius: 18px;
padding: 22px;
box-shadow: 0 14px 36px rgb(33 56 130 / 9%);
}
.hero__badge {
display: inline-block;
font-size: 0.85rem;
margin: 0 0 10px;
color: #2c4bc7;
background: #ecf1ff;
padding: 4px 10px;
border-radius: 999px;
}
h1 {
margin: 0;
font-size: clamp(1.8rem, 2.8vw, 2.8rem);
line-height: 1.18;
}
.hero__description {
margin: 14px 0 0;
line-height: 1.6;
color: #455987;
}
.hero__chips {
margin-top: 14px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.hero__chips span {
border: 1px solid #dce5ff;
border-radius: 999px;
padding: 6px 10px;
font-size: 0.84rem;
color: #3550b3;
background: #f6f9ff;
}
.hero__actions {
margin-top: 18px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.hero__card-title {
margin: 0 0 12px;
font-weight: 800;
color: #21325f;
}
.hero__card ul {
margin: 0;
padding-left: 18px;
display: grid;
gap: 10px;
}
.section {
margin-top: 24px;
scroll-margin-top: 18px;
}
.section h2 {
margin: 0 0 16px;
font-size: clamp(1.4rem, 2vw, 2rem);
}
.section_muted {
background: linear-gradient(180deg, #f9fbff 0%, #f4f8ff 100%);
border-radius: 16px;
border: 1px solid #e8efff;
padding: 20px;
}
.section__head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
}
.grid {
display: grid;
gap: 14px;
}
.grid_3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.card {
background: #ffffff;
border: 1px solid #e1e9ff;
border-radius: 14px;
padding: 16px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover,
.step:hover {
transform: translateY(-3px);
box-shadow: 0 12px 22px rgb(54 79 157 / 12%);
}
.card h3 {
margin: 0 0 10px;
}
.card p {
margin: 0;
line-height: 1.5;
color: #455883;
}
.steps {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.step {
background: #ffffff;
border: 1px solid #e2eaff;
border-radius: 14px;
padding: 16px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.step__index {
color: #3a5bd0;
font-weight: 700;
}
.step h3 {
margin: 10px 0 8px;
}
.step p {
margin: 0;
color: #475c89;
}
.card_pricing ul {
margin: 0 0 14px;
padding-left: 18px;
color: #3f5180;
}
.price {
margin: 0;
font-size: 1.9rem;
font-weight: 700;
color: #13254f;
}
.period {
margin: 4px 0 12px;
color: #4f6493;
}
.card_active {
border-color: #4162f5;
box-shadow: 0 0 0 2px #e3eaff;
}
.lead-form {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
align-items: end;
}
.lead-form label {
display: grid;
gap: 6px;
font-weight: 600;
color: #2e406b;
}
.lead-form input {
border: 1px solid #cad7ff;
border-radius: 10px;
padding: 11px 12px;
font: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.lead-form input:focus {
outline: none;
border-color: #4c6cff;
box-shadow: 0 0 0 3px rgb(76 108 255 / 18%);
}
.status {
margin: 12px 0 0;
color: #2447bb;
}
.faq {
display: grid;
gap: 10px;
}
.faq__item {
border: 1px solid #dde7ff;
border-radius: 14px;
background: #fff;
overflow: hidden;
}
.faq__question {
width: 100%;
background: #fff;
border: 0;
padding: 14px 16px;
display: flex;
justify-content: space-between;
align-items: center;
text-align: left;
font: inherit;
font-weight: 600;
color: #253968;
cursor: pointer;
}
.faq__icon {
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #eef3ff;
color: #3454d6;
font-size: 1.1rem;
}
.faq__answer {
margin: 0;
padding: 0 16px 14px;
color: #516698;
line-height: 1.55;
}
@media (max-width: 960px) {
.hero {
grid-template-columns: 1fr;
}
.grid_3,
.steps {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lead-form {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 760px) {
.landing {
padding: 14px 14px 28px;
}
.menu-toggle {
display: block;
}
.nav {
display: none;
position: absolute;
top: 62px;
right: 14px;
left: 14px;
background: #ffffff;
border-radius: 12px;
padding: 10px;
border: 1px solid #e4e9f9;
box-shadow: 0 10px 24px rgb(23 38 81 / 10%);
flex-direction: column;
align-items: stretch;
z-index: 2;
}
.nav_open {
display: flex;
}
.section__head {
flex-direction: column;
align-items: flex-start;
}
.grid_3,
.steps,
.lead-form {
grid-template-columns: 1fr;
}
}
_____________________________________landing page spec ts_______________________________________________________________
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LandingPage } from './landing-page';
describe('LandingPage', () => {
let component: LandingPage;
let fixture: ComponentFixture<LandingPage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LandingPage],
}).compileComponents();
fixture = TestBed.createComponent(LandingPage);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
______________________________spec ts шляпа________________________________________
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
type Plan = {
title: string;
price: string;
period: string;
features: string[];
cta: string;
};
type FaqItem = {
question: string;
answer: string;
};
@Component({
selector: 'lib-landing-page',
imports: [CommonModule],
templateUrl: './landing-page.html',
styleUrl: './landing-page.scss',
})
export class LandingPage {
isMobileMenuOpen = false;
selectedPlanTitle = 'Старт';
leadStatus = '';
readonly benefits = [
{
title: 'Оценка без визита',
description:
'Заполняйте заявку онлайн и получайте результат без личного присутствия в офисе.',
},
{
title: 'Прозрачные сроки',
description:
'Фиксированные этапы выполнения с уведомлениями о каждом изменении статуса.',
},
{
title: 'Юридическая точность',
description:
'Шаблоны и проверки помогают подготовить документы к нотариальному процессу.',
},
];
readonly steps = [
{
title: 'Заявка за 1 минуту',
description: 'Оставьте контакты и получите обратную связь.',
},
{
title: 'Проверка документов',
description: 'Подскажем, чего не хватает для быстрого старта.',
},
{
title: 'Готовый отчет',
description: 'Запускаем оценку и отправляем итог в личный кабинет.',
},
];
readonly plans: Plan[] = [
{
title: 'Старт',
price: '2 900',
period: 'за заявку',
cta: 'Выбрать Старт',
features: ['Проверка документов', 'Подсказки по заполнению', 'Email-уведомления'],
},
{
title: 'Стандарт',
price: '4 900',
period: 'за заявку',
cta: 'Выбрать Стандарт',
features: ['Все из Старт', 'Приоритетная обработка', 'Поддержка в чате'],
},
{
title: 'Эксперт',
price: '7 900',
period: 'за заявку',
cta: 'Выбрать Эксперт',
features: ['Все из Стандарт', 'Персональный консультант', 'Расширенный итоговый отчет'],
},
];
readonly faq: FaqItem[] = [
{
question: 'Можно ли начать как гость без регистрации?',
answer:
'Да, вы можете оставить заявку и получить консультацию. Для статусов и документов менеджер поможет завершить регистрацию.',
},
{
question: 'Сколько занимает оценка?',
answer:
'В среднем 1-3 рабочих дня. Срок зависит от полноты документов и выбранного тарифа.',
},
{
question: 'Есть ли скрытые платежи?',
answer: 'Нет, цена фиксируется выбранным тарифом. Дополнительные услуги согласовываются заранее.',
},
];
readonly openedFaqIndexes = new Set<number>([0]);
toggleMobileMenu(): void {
this.isMobileMenuOpen = !this.isMobileMenuOpen;
}
scrollToSection(sectionId: string): void {
const section = globalThis.document?.getElementById(sectionId);
if (!section) {
return;
}
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
this.isMobileMenuOpen = false;
}
selectPlan(planTitle: string): void {
this.selectedPlanTitle = planTitle;
this.leadStatus = `Выбран тариф: ${planTitle}. Это stub-действие для будущей интеграции оплаты.`;
}
toggleFaq(index: number): void {
if (this.openedFaqIndexes.has(index)) {
this.openedFaqIndexes.delete(index);
return;
}
this.openedFaqIndexes.add(index);
}
isFaqOpen(index: number): boolean {
return this.openedFaqIndexes.has(index);
}
submitLead(name: string, phone: string): void {
if (!name.trim() || !phone.trim()) {
this.leadStatus = 'Заполните имя и телефон, чтобы отправить заявку.';
return;
}
this.leadStatus = `Спасибо, ${name}! Заявка принята. Мы перезвоним по номеру ${phone}. (stub)`;
}
}