Загрузка данных
<div class="landing">
<header class="header">
<a class="brand" href="#" aria-label="Главная">EstateValue</a>
<button class="menu-toggle" type="button" (click)="toggleMobileMenu()" aria-label="Открыть меню">
<span></span>
<span></span>
<span></span>
</button>
<nav class="nav" [class.nav_open]="isMobileMenuOpen">
<a href="#benefits">Преимущества</a>
<a href="#how-it-works">Как это работает</a>
<a href="#pricing">Тарифы</a>
<a href="#faq">FAQ</a>
<a class="button button_secondary" href="#lead-form">Оставить заявку</a>
</nav>
</header>
<main>
<section class="hero">
<div class="hero__content">
<p class="hero__badge">Роль: Гость</p>
<h1>Оценка наследственного имущества онлайн</h1>
<p class="hero__description">
Продающая landing page с быстрым стартом: оставьте заявку, получите консультацию и начните оценку
в удобном формате.
</p>
<div class="hero__actions">
<a class="button" href="#lead-form">Начать сейчас</a>
<a class="button button_secondary" href="#pricing">Смотреть тарифы</a>
</div>
</div>
<aside class="hero__card" aria-label="Ключевые показатели">
<p class="hero__card-title">Почему выбирают нас</p>
<ul>
<li>95% заявок обрабатываем в день обращения</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 id="how-it-works" 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>JS-интерактив: форма отправляется как stub, чтобы подготовить место под API-интеграцию.</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>{{ isFaqOpen(idx) ? '−' : '+' }}</span>
</button>
@if (isFaqOpen(idx)) {
<p class="faq__answer">{{ item.answer }}</p>
}
</article>
}
</div>
</section>
</main>
</div>
:host {
display: block;
background: linear-gradient(180deg, #f6f8ff 0%, #ffffff 320px);
color: #18223a;
font-family: Inter, Arial, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
.landing {
max-width: 1200px;
margin: 0 auto;
padding: 16px 20px 48px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 8px 0 20px;
}
.brand {
color: #18223a;
text-decoration: none;
font-size: 1.2rem;
font-weight: 700;
}
.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 a {
color: #24355d;
text-decoration: none;
font-weight: 500;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #3e63dd;
border-radius: 10px;
background: #3e63dd;
color: #fff;
text-decoration: none;
padding: 10px 16px;
font-weight: 600;
cursor: pointer;
transition: 0.2s ease;
}
.button:hover {
background: #3452bf;
}
.button_secondary {
background: #ffffff;
color: #2d4ec8;
}
.button_secondary:hover {
background: #edf2ff;
}
.hero {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 20px;
align-items: stretch;
margin-bottom: 22px;
}
.hero__content,
.hero__card {
background: #ffffff;
border: 1px solid #e5e9f7;
border-radius: 18px;
padding: 24px;
box-shadow: 0 8px 22px rgb(34 54 114 / 6%);
}
.hero__badge {
display: inline-block;
font-size: 0.85rem;
margin: 0 0 10px;
color: #2445b8;
background: #e8efff;
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.55;
color: #3c4f77;
}
.hero__actions {
margin-top: 18px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.hero__card-title {
margin: 0 0 12px;
font-weight: 700;
}
.hero__card ul {
margin: 0;
padding-left: 18px;
display: grid;
gap: 10px;
}
.section {
margin-top: 34px;
}
.section h2 {
margin: 0 0 16px;
font-size: clamp(1.4rem, 2vw, 2rem);
}
.section_muted {
background: #f8faff;
border-radius: 16px;
border: 1px solid #edf1ff;
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 #e6ebfb;
border-radius: 14px;
padding: 16px;
}
.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 #dfe7fb;
border-radius: 14px;
padding: 16px;
}
.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: #3e63dd;
box-shadow: 0 0 0 1px #3e63dd;
}
.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 #ccd6f1;
border-radius: 10px;
padding: 11px 12px;
font: inherit;
}
.status {
margin: 12px 0 0;
color: #1f3f9f;
}
.faq {
display: grid;
gap: 10px;
}
.faq__item {
border: 1px solid #dee6fb;
border-radius: 12px;
background: #ffffff;
}
.faq__question {
width: 100%;
background: transparent;
border: 0;
padding: 14px 16px;
display: flex;
justify-content: space-between;
align-items: center;
font: inherit;
text-align: left;
cursor: pointer;
}
.faq__answer {
margin: 0;
padding: 0 16px 14px;
color: #4c6090;
}
@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: 12px;
border: 1px solid #e4e9f9;
box-shadow: 0 10px 24px rgb(23 38 81 / 10%);
flex-direction: column;
align-items: stretch;
}
.nav_open {
display: flex;
}
.section__head {
flex-direction: column;
align-items: flex-start;
}
.grid_3,
.steps,
.lead-form {
grid-template-columns: 1fr;
}
}
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();
});
});
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: 'Оставьте заявку',
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;
}
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)`;
}
}