Загрузка данных


<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)`;
  }
}