# Живые части: «шоурум открыт» и калькулятор кухни

Все главы  [1 Перед началом](https://eile.site/cookbook/nachalo)[2 Установка](https://eile.site/cookbook/ustanovka)[3 Страницы](https://eile.site/cookbook/stranitsy)[4 Первый шаблон](https://eile.site/cookbook/pervyi-shablon)[5 Компоненты](https://eile.site/cookbook/komponenty)[6 Рамка сайта](https://eile.site/cookbook/ramka)[7 Поля страниц](https://eile.site/cookbook/polya)[8 Списки страниц](https://eile.site/cookbook/spiski)[9 Свои настройки](https://eile.site/cookbook/nastroiki)[10 Заявка на замер](https://eile.site/cookbook/forma)[11 Живые части](https://eile.site/cookbook/zhivye-chasti)[12 В интернет](https://eile.site/cookbook/v-internet)[13 Что дальше](https://eile.site/cookbook/dalshe)

# Живые части: «шоурум открыт» и калькулятор кухни

Страницы Eile готовятся один раз и отдаются из памяти мгновенно. Но кое-что должно меняться: открыт ли шоурум прямо сейчас, сколько будет стоить кухня длиной 3,4 метра. Для этого есть некэшируемые регионы и острова.

Глава 11 из 13 · 5 минут чтения

## Как работает кэш

Когда посетитель впервые открывает страницу, Eile собирает её по шаблону и запоминает готовый HTML. Следующие посетители получают его из памяти за доли миллисекунды. Как только вы поправите страницу или шаблон, запомненное сбрасывается, и страница соберётся заново.

Это хорошо для всего, что меняется от правки к правке. А полоска «Шоурум открыт» меняется сама, по часам. Запомни её Eile в десять утра, она бы весь день показывала то, что было в десять.

## Некэшируемый регион

Для таких мест есть регион: кусок страницы, который собирается заново на каждый запрос, а всё вокруг него по-прежнему берётся из памяти. Сделаем компонент `Showroom`:

Компонент Showroom

```
<!-- @component
Открыт ли шоурум: считается заново на каждый запрос по московскому времени.
-->
<script>
  let {
    /** Открываемся, час */
    opens = 10,
    /** Закрываемся, час */
    closes = 20,
  } = $props();

  const hour = Number(
    new Intl.DateTimeFormat('ru', {
      hour: 'numeric',
      hourCycle: 'h23',
      timeZone: 'Europe/Moscow',
    }).format(new Date())
  );
  const open = hour >= opens && hour < closes;
</script>

<p class="now" class:open>
  {#if open}
    Шоурум открыт до {closes}:00, приходите посмотреть кухни вживую
  {:else}
    Шоурум закрыт, откроемся в {opens}:00
  {/if}
</p>

<style>
  .now {
    margin: 0;
    padding: 0.55rem 1.5rem;
    background: #1f1e1c;
    color: #f1efeb;
    font-size: 0.875rem;
    text-align: center;
  }

  .open {
    background: #3d4733;
  }
</style>
```

- `new Date()`: текущий момент. `Intl.DateTimeFormat` с `timeZone: 'Europe/Moscow'` переводит его в час по Москве, где бы ни стоял сервер.
- `class:open`: добавляет класс `open`, когда шоурум открыт. Так у полоски меняется цвет.

Теперь поставим полоску в шапку как регион. Замените код Header:

Компонент Header

```
<!-- @component
Шапка: открыт ли шоурум, название сайта и меню.
-->
<script>
  import Nav from '$components/Nav';
  import { site } from '$core';
  import Uncached from '$core/Uncached';
</script>

<Uncached name="Showroom" props={{ opens: 10, closes: 20 }} />

<header class="header">
  <a class="logo" href="/">{site().name}</a>
  <Nav />
</header>

<style>
  .header {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    max-width: 48rem;
    margin: 0 auto;
    padding: 1.5rem;
  }

  .logo {
    color: inherit;
    font-size: 1.25rem;
    font-weight: 800;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    text-decoration: none;
  }
</style>
```

`<Uncached name="Showroom" …/>` говорит: «этот компонент не запоминай, рисуй каждый раз». Параметры передаются в `props`.

## Остров: калькулятор кухни

Регион собирается на сервере. А калькулятор должен отзываться на ползунок без перезагрузки, то есть работать в браузере. Для этого есть острова: компонент, который сервер рисует как обычно, а браузер потом оживляет. JavaScript попадает только на те страницы, где остров есть. Остальной сайт остаётся без единого скрипта.

Компонент `Calc`:

Компонент Calc

```
<!-- @component
Калькулятор: длина кухни в метрах и примерная цена. Считает прямо в браузере.
-->
<script>
  let {
    /** Цена за погонный метр, ₽ */
    price = 0,
  } = $props();

  let length = $state(3);
  const rub = new Intl.NumberFormat('ru');
</script>

<div class="calc">
  <label for="length">Длина кухни: <b>{length.toLocaleString('ru')} м</b></label>
  <input id="length" type="range" min="1.5" max="6" step="0.1" bind:value={length} />
  <p>Примерно <b>{rub.format(Math.round(price * length))} ₽</b> без техники и сборки</p>
</div>

<style>
  .calc {
    display: grid;
    gap: 0.6rem;
    margin: 1.5rem 0;
    padding: 1.25rem 1.4rem;
    border-radius: 1.25rem;
    background: #fff;
  }

  input {
    width: 100%;
    accent-color: #1f1e1c;
  }

  p {
    margin: 0;
  }
</style>
```

- `let length = $state(3);`: состояние, число, которое может меняться. Сначала кухня длиной три метра.
- `bind:value={length}`: связывает ползунок и переменную в обе стороны. Двигаете ползунок, меняется `length`, а за ним всё, что от неё зависит.
- `{rub.format(Math.round(price * length))}`: цена пересчитывается при каждом движении ползунка.

Поставим остров на страницу кухни. Замените код шаблона Kitchen:

Шаблон Kitchen

```
<script>
  import Content from '$core/Content';
  import Island from '$core/Island';
  import Layout from '$components/Layout';
  import { image } from '$core';

  let { page } = $props();

  const price = page.fields.price;
  const plan = page.fields.plan;
  const photo = page.fields.photo ? image(page.fields.photo) : null;
  const rub = new Intl.NumberFormat('ru');
</script>

<Layout {page}>
  {#if photo}
    <img
      class="photo"
      src={photo.src}
      srcset={photo.srcset}
      sizes="(min-width: 48rem) 45rem, 100vw"
      width={photo.width}
      height={photo.height}
      alt={photo.alt}
    />
  {/if}

  <h1>{page.title}</h1>

  <dl class="facts">
    {#if plan}
      <div><dt>Планировка</dt><dd>{plan}</dd></div>
    {/if}
    {#if price}
      <div><dt>Цена</dt><dd>от {rub.format(price)} ₽ за метр</dd></div>
    {/if}
  </dl>

  {#if price}
    <Island name="Calc" props={{ price }} />
  {/if}

  <Content of={page} />
</Layout>

<style>
  .photo {
    display: block;
    border-radius: 1.25rem;
  }

  .facts {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 2.5rem;
    margin: 0;
  }

  dt {
    color: #6b6862;
    font-size: 0.875rem;
  }

  dd {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 650;
  }
</style>
```

`<Island name="Calc" props={{ price }} />`: цена уходит острову параметром. Острову недоступен `$core`: он живёт в браузере, где нет базы сайта. Всё нужное передавайте через `props`.

![Страница кухни с калькулятором стоимости](https://eile.site/media/screens/cook-calc-light.webp?v=8b69819533fa01ff)

Страница кухни с калькулятором стоимости