HTML Diff
1 added 1 removed
Original 2026-01-01
Modified 2026-02-21
1 <p><a>#статьи</a></p>
1 <p><a>#статьи</a></p>
2 <ul><li>19 сен 2022</li>
2 <ul><li>19 сен 2022</li>
3 <li>0</li>
3 <li>0</li>
4 </ul><p>Создаём веб-приложение для быстрого заполнения расписания новостной передачи.</p>
4 </ul><p>Создаём веб-приложение для быстрого заполнения расписания новостной передачи.</p>
5 <p>Иллюстрация: Катя Павловская для Skillbox Media</p>
5 <p>Иллюстрация: Катя Павловская для Skillbox Media</p>
6 <p>Готовить эфиры бывает утомительно и непросто, поэтому, помимо ведущих, за кадром должна быть сильная команда помощников. И неважно, идёт ли речь о передаче на телевидении или на YouTube, Rutube, в VK и так далее: главные герои тыла - продюсеры, которые ищут экспертов, договариваются с гостями, предлагают темы для обсуждения, согласовывают всё это и составляют для всех расписания.</p>
6 <p>Готовить эфиры бывает утомительно и непросто, поэтому, помимо ведущих, за кадром должна быть сильная команда помощников. И неважно, идёт ли речь о передаче на телевидении или на YouTube, Rutube, в VK и так далее: главные герои тыла - продюсеры, которые ищут экспертов, договариваются с гостями, предлагают темы для обсуждения, согласовывают всё это и составляют для всех расписания.</p>
7 <p>Миллиарды нервных клеток продюсеров стримов сгорели в попытках находить интересных героев, делать выпуски непохожими друг на друга, возвращать на связь внезапно исчезнувших с радаров гостей и, конечно, вписываться в график. В общем, продюсер эфира - этакий рокетмен, сжигающий свои предохранители. Но как тут может помочь программирование? Давайте попробуем разобраться.</p>
7 <p>Миллиарды нервных клеток продюсеров стримов сгорели в попытках находить интересных героев, делать выпуски непохожими друг на друга, возвращать на связь внезапно исчезнувших с радаров гостей и, конечно, вписываться в график. В общем, продюсер эфира - этакий рокетмен, сжигающий свои предохранители. Но как тут может помочь программирование? Давайте попробуем разобраться.</p>
8 <p>Опыт участия в подготовке новостных эфиров, а также обсуждения темы с друзьями из других медиа заставили задуматься: а вдруг в работе продюсера, помимо творчества, есть шаблонные действия? Ведь, если это правда, часть задач можно поручить компьютеру! А это снижение нагрузки, освобождение части рабочего времени и прочее.</p>
8 <p>Опыт участия в подготовке новостных эфиров, а также обсуждения темы с друзьями из других медиа заставили задуматься: а вдруг в работе продюсера, помимо творчества, есть шаблонные действия? Ведь, если это правда, часть задач можно поручить компьютеру! А это снижение нагрузки, освобождение части рабочего времени и прочее.</p>
9 <p>Предположим, что некий алгоритм действительно существует, хотя и различается в разных редакциях. Вот что может учитывать условный продюсер стрима на условную политическую тематику.</p>
9 <p>Предположим, что некий алгоритм действительно существует, хотя и различается в разных редакциях. Вот что может учитывать условный продюсер стрима на условную политическую тематику.</p>
10 <p>При ежедневных выходах выбор как минимум между сегодня и завтра. Повестка быстро меняется, и планировать дальше может быть сложно - хотя когда как.</p>
10 <p>При ежедневных выходах выбор как минимум между сегодня и завтра. Повестка быстро меняется, и планировать дальше может быть сложно - хотя когда как.</p>
11 <p>Допустим, утро или вечер - для разной ЦА. Кто-то внимательно посмотрит прямую трансляцию, кто-то хочет послушать эфир за рулём, кто-то на кухне за готовкой, некоторые подписчики включат его фоном во время работы, а остальные посмотрят запись на досуге.</p>
11 <p>Допустим, утро или вечер - для разной ЦА. Кто-то внимательно посмотрит прямую трансляцию, кто-то хочет послушать эфир за рулём, кто-то на кухне за готовкой, некоторые подписчики включат его фоном во время работы, а остальные посмотрят запись на досуге.</p>
12 <p>Обычно это один или несколько человек из списка постоянных ведущих. Кто-то из них может быть в этот день недоступен по причине занятости, командировки, отпуска, болезни.</p>
12 <p>Обычно это один или несколько человек из списка постоянных ведущих. Кто-то из них может быть в этот день недоступен по причине занятости, командировки, отпуска, болезни.</p>
13 <p>Это самый интересный и творческий пункт. Задача в том, чтобы определить самые актуальные темы и найти гостей (экспертов), которые впишутся в передачу и будут согласны ненадолго выйти в эфир с комментарием.</p>
13 <p>Это самый интересный и творческий пункт. Задача в том, чтобы определить самые актуальные темы и найти гостей (экспертов), которые впишутся в передачу и будут согласны ненадолго выйти в эфир с комментарием.</p>
14 <p>Но это ещё не всё. Хороших экспертов нужно взять на заметку и приглашать снова, а плохих - отсеять.</p>
14 <p>Но это ещё не всё. Хороших экспертов нужно взять на заметку и приглашать снова, а плохих - отсеять.</p>
15 <p>Когда расписание в том или ином виде составлено, его нужно согласовать с ведущими. С точки зрения подачи это означает, что текст должен быть ясным, кратким и выглядеть аккуратно, потому что ведущие - занятые люди и у них нет времени читать полотна текста.</p>
15 <p>Когда расписание в том или ином виде составлено, его нужно согласовать с ведущими. С точки зрения подачи это означает, что текст должен быть ясным, кратким и выглядеть аккуратно, потому что ведущие - занятые люди и у них нет времени читать полотна текста.</p>
16 <p>Таким образом, есть несколько плавающих переменных, которые способны измениться в очень короткий срок. Но всё же это одни и те же переменные, и сбор информации действительно можно немножечко автоматизировать. Это мы и сделаем.</p>
16 <p>Таким образом, есть несколько плавающих переменных, которые способны измениться в очень короткий срок. Но всё же это одни и те же переменные, и сбор информации действительно можно немножечко автоматизировать. Это мы и сделаем.</p>
17 <p>Наша программа будет простой и наглядной. Мы создадим локальную веб-страницу с самым необходимым:</p>
17 <p>Наша программа будет простой и наглядной. Мы создадим локальную веб-страницу с самым необходимым:</p>
18 <ul><li>приятным интерфейсом;</li>
18 <ul><li>приятным интерфейсом;</li>
19 <li>возможностью выбора опций эфира;</li>
19 <li>возможностью выбора опций эфира;</li>
20 <li>небольшой базой данных экспертов с возможностью добавления новых;</li>
20 <li>небольшой базой данных экспертов с возможностью добавления новых;</li>
21 <li>автоматическим заполнением текста расписания;</li>
21 <li>автоматическим заполнением текста расписания;</li>
22 <li>отправкой оформленного поста в рабочий Telegram-чат команды стрима.</li>
22 <li>отправкой оформленного поста в рабочий Telegram-чат команды стрима.</li>
23 </ul><p>В результате продюсер эфира сможет открыть нашу страницу, выбрать нужные детали и гостей (при необходимости - завести карточки новых гостей), составить расписание, почти ничего не печатая, и в один клик переслать структурированный план на согласование ведущим. Для многих этого будет вполне достаточно, хотя при желании можно запросто накрутить дополнительные возможности.</p>
23 </ul><p>В результате продюсер эфира сможет открыть нашу страницу, выбрать нужные детали и гостей (при необходимости - завести карточки новых гостей), составить расписание, почти ничего не печатая, и в один клик переслать структурированный план на согласование ведущим. Для многих этого будет вполне достаточно, хотя при желании можно запросто накрутить дополнительные возможности.</p>
24 <p>Какие технологии будем использовать:</p>
24 <p>Какие технологии будем использовать:</p>
25 <ul><li>HTML и CSS для красивого дизайна;</li>
25 <ul><li>HTML и CSS для красивого дизайна;</li>
26 <li>язык JavaScript для программирования интерфейса;</li>
26 <li>язык JavaScript для программирования интерфейса;</li>
27 <li>API IndexedDB для создания локальной базы данных прямо в браузере (современными браузерами оно поддерживается);</li>
27 <li>API IndexedDB для создания локальной базы данных прямо в браузере (современными браузерами оно поддерживается);</li>
28 <li>Telegram Bot API для отправки расписания.</li>
28 <li>Telegram Bot API для отправки расписания.</li>
29 </ul><p>Готовый код мы разместили на pastebin.com:</p>
29 </ul><p>Готовый код мы разместили на pastebin.com:</p>
30 <ul><li><a>HTML-код</a>;</li>
30 <ul><li><a>HTML-код</a>;</li>
31 <li><a>JavaScript-код</a>.</li>
31 <li><a>JavaScript-код</a>.</li>
32 </ul><p>Допустим, мы сделаем приложение для продюсеров условной YouTube-передачи Skillbox FM с реальными ведущими и гостями (по мотивам уже вышедших эпизодов подкаста "<a>Люди и код</a>").</p>
32 </ul><p>Допустим, мы сделаем приложение для продюсеров условной YouTube-передачи Skillbox FM с реальными ведущими и гостями (по мотивам уже вышедших эпизодов подкаста "<a>Люди и код</a>").</p>
33 Приложение для автоматизации подготовки эфиров - общий вид<em>Скриншот: Skillbox Media</em><p>Приложение будет следовать логике продюсера новостного эфира: определять дату, время и ведущих. А после этого сопоставлять экспертов и время выхода. В конце приложение покажет расписание всем ведущим.</p>
33 Приложение для автоматизации подготовки эфиров - общий вид<em>Скриншот: Skillbox Media</em><p>Приложение будет следовать логике продюсера новостного эфира: определять дату, время и ведущих. А после этого сопоставлять экспертов и время выхода. В конце приложение покажет расписание всем ведущим.</p>
34 <p>Взглянем на всё это как программисты:</p>
34 <p>Взглянем на всё это как программисты:</p>
35 <ul><li>по сути, нам надо сделать набор чекбоксов, которые администратор проставляет в нужном порядке;</li>
35 <ul><li>по сути, нам надо сделать набор чекбоксов, которые администратор проставляет в нужном порядке;</li>
36 <li>выбор времени подключения привязан к блоку "Вид эфира" и устанавливается, когда отмечаем утро или вечер;</li>
36 <li>выбор времени подключения привязан к блоку "Вид эфира" и устанавливается, когда отмечаем утро или вечер;</li>
37 <li>для добавления экспертов в расписание к конкретному времени нужно по очереди щёлкнуть на время подключения, а затем на имя гостя - и они встанут на свои места;</li>
37 <li>для добавления экспертов в расписание к конкретному времени нужно по очереди щёлкнуть на время подключения, а затем на имя гостя - и они встанут на свои места;</li>
38 <li>все опции сконцентрированы вокруг требуемого результата (текста поста). Поэтому блок "Предпросмотр поста" должен располагаться по центру.</li>
38 <li>все опции сконцентрированы вокруг требуемого результата (текста поста). Поэтому блок "Предпросмотр поста" должен располагаться по центру.</li>
39 </ul><p>Вот как отреагирует программа, если мы выберем случайные опции.</p>
39 </ul><p>Вот как отреагирует программа, если мы выберем случайные опции.</p>
40 Выбраны опции для утреннего эфира<em>Скриншот: Skillbox Media</em>Выбраны опции для вечернего эфира<em>Скриншот: Skillbox Media</em><p>Нажав на голубую кнопку вверху, мы отправим в Telegram-чат вот такой пост.</p>
40 Выбраны опции для утреннего эфира<em>Скриншот: Skillbox Media</em>Выбраны опции для вечернего эфира<em>Скриншот: Skillbox Media</em><p>Нажав на голубую кнопку вверху, мы отправим в Telegram-чат вот такой пост.</p>
41 Расписание утреннего эфира - результат отправки в чат<em>Скриншот: Skillbox Media</em>Расписание вечернего эфира - результат отправки в чат<em>Скриншот: Skillbox Media</em><p>Теперь разберём, как написать такую программу.</p>
41 Расписание утреннего эфира - результат отправки в чат<em>Скриншот: Skillbox Media</em>Расписание вечернего эфира - результат отправки в чат<em>Скриншот: Skillbox Media</em><p>Теперь разберём, как написать такую программу.</p>
42 <p>Код будет упакован в два файла: Air Constructor.html и speakersDB.js. Первый - сама страница (HTML, CSS и немного JavaScript). Второй - всё, что связано с базой данных экспертов (JavaScript-код, который мы подключим к веб-странице).</p>
42 <p>Код будет упакован в два файла: Air Constructor.html и speakersDB.js. Первый - сама страница (HTML, CSS и немного JavaScript). Второй - всё, что связано с базой данных экспертов (JavaScript-код, который мы подключим к веб-странице).</p>
43 <p>Посмотрим на применение указанных инструментов.</p>
43 <p>Посмотрим на применение указанных инструментов.</p>
44 <p>Наш интерфейс должен быть не только приятным, но и привычным для продюсера, поэтому его нужно оформить в фирменном стиле медиа. В <a>нашем</a>случае ведущий цвет - синий (код #3D3BFF).</p>
44 <p>Наш интерфейс должен быть не только приятным, но и привычным для продюсера, поэтому его нужно оформить в фирменном стиле медиа. В <a>нашем</a>случае ведущий цвет - синий (код #3D3BFF).</p>
45 <p>С вёрсткой мудрить не будем:</p>
45 <p>С вёрсткой мудрить не будем:</p>
46 <ul><li>элементарная сетка из трёх &lt;div&gt;-блоков для разделения страницы на три вертикальных уровня (&lt;div id="firstBlock"&gt;, &lt;div id="secondBlock"&gt;, &lt;div id="thirdBlock"&gt;);</li>
46 <ul><li>элементарная сетка из трёх &lt;div&gt;-блоков для разделения страницы на три вертикальных уровня (&lt;div id="firstBlock"&gt;, &lt;div id="secondBlock"&gt;, &lt;div id="thirdBlock"&gt;);</li>
47 <li>первый уровень (см. скриншот) - заголовок "Расписание эфира" и кнопка отправки на синем фоне;</li>
47 <li>первый уровень (см. скриншот) - заголовок "Расписание эфира" и кнопка отправки на синем фоне;</li>
48 <li>второй уровень - блоки "Дата эфира", "Вид эфира", "Ведущие";</li>
48 <li>второй уровень - блоки "Дата эфира", "Вид эфира", "Ведущие";</li>
49 <li>третий уровень - блоки "Время подключения", "Предпросмотр поста", "Спикеры";</li>
49 <li>третий уровень - блоки "Время подключения", "Предпросмотр поста", "Спикеры";</li>
50 <li>CSS-правил будет немного, поэтому разместим их не в отдельном файле, а внутри страницы с помощью элемента &lt;style&gt;.</li>
50 <li>CSS-правил будет немного, поэтому разместим их не в отдельном файле, а внутри страницы с помощью элемента &lt;style&gt;.</li>
51 </ul><p>С опциями интерфейса в целом всё тоже просто: чтобы дать пользователю возможность выбора пунктов, вставим в наши &lt;div&gt;-блоки шесть HTML-форм (элементы &lt;form&gt;), внутри которых будут связанные элементы &lt;input&gt;/&lt;label&gt;, &lt;fieldset&gt;/&lt;legend&gt; или &lt;textarea&gt;.</p>
51 </ul><p>С опциями интерфейса в целом всё тоже просто: чтобы дать пользователю возможность выбора пунктов, вставим в наши &lt;div&gt;-блоки шесть HTML-форм (элементы &lt;form&gt;), внутри которых будут связанные элементы &lt;input&gt;/&lt;label&gt;, &lt;fieldset&gt;/&lt;legend&gt; или &lt;textarea&gt;.</p>
52 <p>Взаимодействие форм с инпутами и подписями поначалу может показаться запутанным, поэтому разберём его подробнее:</p>
52 <p>Взаимодействие форм с инпутами и подписями поначалу может показаться запутанным, поэтому разберём его подробнее:</p>
53 <ul><li>форма (элемент<a>&lt;form&gt;</a>) - это секция документа, содержащая интерактивные элементы для отправки информации. Содержит элементы формы (см. ниже);</li>
53 <ul><li>форма (элемент<a>&lt;form&gt;</a>) - это секция документа, содержащая интерактивные элементы для отправки информации. Содержит элементы формы (см. ниже);</li>
54 <li>инпут (элемент<a>&lt;input&gt;</a>) - это как раз элемент управления одного из нескольких возможных типов. Мы будем использовать инпуты типа radio (выбор только одного варианта) и checkbox (выбор любого количества вариантов);</li>
54 <li>инпут (элемент<a>&lt;input&gt;</a>) - это как раз элемент управления одного из нескольких возможных типов. Мы будем использовать инпуты типа radio (выбор только одного варианта) и checkbox (выбор любого количества вариантов);</li>
55 <li>элемент<a>&lt;label&gt;</a> - это подпись для связанного инпута. Связь указывается с помощью атрибутов id и for;</li>
55 <li>элемент<a>&lt;label&gt;</a> - это подпись для связанного инпута. Связь указывается с помощью атрибутов id и for;</li>
56 <li>элемент<a>&lt;fieldset&gt;</a>группирует несколько управляющих элементов формы, а связанный с ним<a>&lt;legend&gt;</a>буквально добавляет над ними "легенду" (красивый заголовок).</li>
56 <li>элемент<a>&lt;fieldset&gt;</a>группирует несколько управляющих элементов формы, а связанный с ним<a>&lt;legend&gt;</a>буквально добавляет над ними "легенду" (красивый заголовок).</li>
57 </ul><p>Мы нарушим эту схему только в двух случаях: для кнопки отправки в Telegram укажем тип submit ("отправка", без подписи с помощью &lt;label&gt;), а форма предпросмотра поста будет содержать только текстовый блок &lt;textarea&gt; для расписания.</p>
57 </ul><p>Мы нарушим эту схему только в двух случаях: для кнопки отправки в Telegram укажем тип submit ("отправка", без подписи с помощью &lt;label&gt;), а форма предпросмотра поста будет содержать только текстовый блок &lt;textarea&gt; для расписания.</p>
58 <p>Давайте теперь настроим отображение этих элементов. Для начала сбросим дефолтные стили браузера, чтобы самостоятельно задать отступы и шрифт.</p>
58 <p>Давайте теперь настроим отображение этих элементов. Для начала сбросим дефолтные стили браузера, чтобы самостоятельно задать отступы и шрифт.</p>
59 * { font-family: Intro Light, sans-serif; margin: 0; padding: 0; }<p>Далее сделаем ширину трёх главных блоков сетки равной ширине страницы (заодно установим и другие опции).</p>
59 * { font-family: Intro Light, sans-serif; margin: 0; padding: 0; }<p>Далее сделаем ширину трёх главных блоков сетки равной ширине страницы (заодно установим и другие опции).</p>
60 #firstBlock { width: 100%; position: relative; } #secondBlock { width: 100%; margin-left: 5%; } #thirdBlock { width: 100%; margin-left: 5%; clear: both; }<p>Позаботимся о стиле заголовка страницы:</p>
60 #firstBlock { width: 100%; position: relative; } #secondBlock { width: 100%; margin-left: 5%; } #thirdBlock { width: 100%; margin-left: 5%; clear: both; }<p>Позаботимся о стиле заголовка страницы:</p>
61 h1 { background-color: #3D3BFF; color: #fff; width: 100%; font-family: Intro Black, sans-serif; text-align: center; padding: 5px; }<p>И стиле кнопки отправки в Telegram:</p>
61 h1 { background-color: #3D3BFF; color: #fff; width: 100%; font-family: Intro Black, sans-serif; text-align: center; padding: 5px; }<p>И стиле кнопки отправки в Telegram:</p>
62 #sendButton { width: 170px; height: 30px; position: absolute; top: 10%; right: 4%; background-color: #179cde; border: 1px dashed white; color: #fff; font-family: Intro Black, sans-serif; cursor: pointer; }<p>А этот стиль поможет правильно выстроить блоки меню относительно друг друга - чтобы они не съезжали со строк и не наезжали друг на друга. Первые три блока - одинаковые.</p>
62 #sendButton { width: 170px; height: 30px; position: absolute; top: 10%; right: 4%; background-color: #179cde; border: 1px dashed white; color: #fff; font-family: Intro Black, sans-serif; cursor: pointer; }<p>А этот стиль поможет правильно выстроить блоки меню относительно друг друга - чтобы они не съезжали со строк и не наезжали друг на друга. Первые три блока - одинаковые.</p>
63 #first_form, #second_form, #third_form { width: 30%; margin-top: 1%; margin-bottom: 1%; margin-right: 5px; float: left; }<p>Формы 4-6 должны быть разными по ширине: больше места под расписание и базу гостей и меньше - для времени подключения. Обратите внимание, что в коде фактически три разных варианта четвёртой формы (выбор времени подключения) - просто отображаться должен только один (об этом ниже).</p>
63 #first_form, #second_form, #third_form { width: 30%; margin-top: 1%; margin-bottom: 1%; margin-right: 5px; float: left; }<p>Формы 4-6 должны быть разными по ширине: больше места под расписание и базу гостей и меньше - для времени подключения. Обратите внимание, что в коде фактически три разных варианта четвёртой формы (выбор времени подключения) - просто отображаться должен только один (об этом ниже).</p>
64 #fourth_form_filler, #fourth_form_morning, #fourth_form_evening { width: 20%; margin-bottom: 1%; margin-right: 5px; float: left; } #fifth_form, #sixth_form { width: 35%; margin-bottom: 1%; margin-right: 5px; float: left; }<p>Отдельные настройки области для текста (элемент &lt;textarea&gt; в пятой форме). В частности, убираем возможность менять её размер (resize: none) и добавляем возможность прокрутки на случай, если текста будет много (ну мало ли).</p>
64 #fourth_form_filler, #fourth_form_morning, #fourth_form_evening { width: 20%; margin-bottom: 1%; margin-right: 5px; float: left; } #fifth_form, #sixth_form { width: 35%; margin-bottom: 1%; margin-right: 5px; float: left; }<p>Отдельные настройки области для текста (элемент &lt;textarea&gt; в пятой форме). В частности, убираем возможность менять её размер (resize: none) и добавляем возможность прокрутки на случай, если текста будет много (ну мало ли).</p>
65 #fifth_form &gt; fieldset { overflow: scroll; } textarea { width: 95%; height: 92%; padding: 2%; font-size: 18px; resize: none; border: 0; }<p>Оставшиеся CSS-правила не так важны - и вы сможете увидеть их в финальном варианте. А мы пойдём к самому интересному - программированию поведения элементов.</p>
65 #fifth_form &gt; fieldset { overflow: scroll; } textarea { width: 95%; height: 92%; padding: 2%; font-size: 18px; resize: none; border: 0; }<p>Оставшиеся CSS-правила не так важны - и вы сможете увидеть их в финальном варианте. А мы пойдём к самому интересному - программированию поведения элементов.</p>
66 <p>Пока ещё не касаясь базы данных, отметим менее очевидные задачи: необходимо сбросить дефолтный выбор пунктов меню и вовремя добавить функции - обработчики событий на клики по различным пунктам.</p>
66 <p>Пока ещё не касаясь базы данных, отметим менее очевидные задачи: необходимо сбросить дефолтный выбор пунктов меню и вовремя добавить функции - обработчики событий на клики по различным пунктам.</p>
67 <p>Этот блок мы добавим ближе к началу кода HTML-страницы - в элемент &lt;head&gt;:</p>
67 <p>Этот блок мы добавим ближе к началу кода HTML-страницы - в элемент &lt;head&gt;:</p>
68 &lt;script&gt; // Функция отправки расписания в Telegram-чат (привязывается к кнопке отправки). function sendSchedule() { // Конструктор ссылки для отправки новостей в Telegram: токен бота, ID чата, способ кодировки, заголовок + текст дайджеста, предупреждение. let token = '12345abcd'; let chat = '-10012345'; let text = encodeURIComponent(document.getElementsByTagName('textarea')[0].value); let sendURL = 'https://api.telegram.org/bot' + token + '/sendMessage?chat_id=' + chat + '&amp;parse_mode=HTML&amp;text=' + text; fetch(sendURL); alert('Расписание отправлено.'); console.log('Отправка расписания в Telegram-чат.'); }; // Функция отмены предварительного выбора пунктов в меню. function uncheckInputs() { var inputs = document.getElementsByTagName('input'); for (var i = 0; i &lt; inputs.length; i++) { inputs[i].checked = false; }; }; &lt;/script&gt;<p>Остановимся ненадолго на реализации отправки поста в Telegram (функция sendSchedule). Нужно всего лишь сделать GET-запрос методом<a>sendMessage()</a>из Telegram Bot API с помощью JavaScript-метода<a>fetch()</a>. Требуется только подставить в нужные переменные токен вашего бота (его можно получить при создании бота у BotFather) и ID чата.</p>
68 &lt;script&gt; // Функция отправки расписания в Telegram-чат (привязывается к кнопке отправки). function sendSchedule() { // Конструктор ссылки для отправки новостей в Telegram: токен бота, ID чата, способ кодировки, заголовок + текст дайджеста, предупреждение. let token = '12345abcd'; let chat = '-10012345'; let text = encodeURIComponent(document.getElementsByTagName('textarea')[0].value); let sendURL = 'https://api.telegram.org/bot' + token + '/sendMessage?chat_id=' + chat + '&amp;parse_mode=HTML&amp;text=' + text; fetch(sendURL); alert('Расписание отправлено.'); console.log('Отправка расписания в Telegram-чат.'); }; // Функция отмены предварительного выбора пунктов в меню. function uncheckInputs() { var inputs = document.getElementsByTagName('input'); for (var i = 0; i &lt; inputs.length; i++) { inputs[i].checked = false; }; }; &lt;/script&gt;<p>Остановимся ненадолго на реализации отправки поста в Telegram (функция sendSchedule). Нужно всего лишь сделать GET-запрос методом<a>sendMessage()</a>из Telegram Bot API с помощью JavaScript-метода<a>fetch()</a>. Требуется только подставить в нужные переменные токен вашего бота (его можно получить при создании бота у BotFather) и ID чата.</p>
69 - <p>Далее констуктор собирает из переменных ссылку для запроса, а чтобы функция срабатывала по клику на кнопку отправки, мы добавляем кнопке обработчик с названием функции:</p>
69 + <p>Далее конструктор собирает из переменных ссылку для запроса, а чтобы функция срабатывала по клику на кнопку отправки, мы добавляем кнопке обработчик с названием функции:</p>
70 &lt;input type="submit" value="Отправить расписание" id="sendButton" onclick="sendSchedule()"&gt;<p>Ещё один скрипт - после отрисовки первой формы "Дата эфира", для которой требуется получить сегодняшнюю и завтрашнюю даты.</p>
70 &lt;input type="submit" value="Отправить расписание" id="sendButton" onclick="sendSchedule()"&gt;<p>Ещё один скрипт - после отрисовки первой формы "Дата эфира", для которой требуется получить сегодняшнюю и завтрашнюю даты.</p>
71 &lt;script&gt; // Определяем и вставляем актуальные даты (сегодня и завтра). let now = new Date(); let now2 = new Date(); now2.setDate(now2.getDate() + 1); let month = [ 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' ]; let today = now.getDate().toString() + ' ' + month[now.getMonth()]; let tomorrow = now2.getDate().toString() + ' ' + month[now2.getMonth()]; document.getElementById('replace1').innerHTML = 'Сегодня, ' + today; document.getElementById('replace2').innerHTML = 'Завтра, ' + tomorrow; &lt;/script&gt;<p>В самый конец тела страницы вставляем вызовы функции отмены предварительного выбора пунктов меню uncheckInputs() и функций - обработчиков кликов по пунктам меню. Приведём часть этого блока - остальное будет по тому же принципу.</p>
71 &lt;script&gt; // Определяем и вставляем актуальные даты (сегодня и завтра). let now = new Date(); let now2 = new Date(); now2.setDate(now2.getDate() + 1); let month = [ 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' ]; let today = now.getDate().toString() + ' ' + month[now.getMonth()]; let tomorrow = now2.getDate().toString() + ' ' + month[now2.getMonth()]; document.getElementById('replace1').innerHTML = 'Сегодня, ' + today; document.getElementById('replace2').innerHTML = 'Завтра, ' + tomorrow; &lt;/script&gt;<p>В самый конец тела страницы вставляем вызовы функции отмены предварительного выбора пунктов меню uncheckInputs() и функций - обработчиков кликов по пунктам меню. Приведём часть этого блока - остальное будет по тому же принципу.</p>
72 // Если выбрано время "утро". function checkMorning() { if (document.getElementById('morning').checked = true) { document.getElementById('fourth_form_filler').setAttribute('style', 'display: none;'); document.getElementById('fourth_form_evening').setAttribute('style', 'display: none;'); document.getElementById('fourth_form_morning').setAttribute('style', 'display: inherit;'); document.getElementsByTagName('textarea')[0].value += 'УТРО' + '\n\n' + 'Ведущие: '; }; }; // Если выбрано время "вечер". function checkEvening() { if (document.getElementById('evening').checked = true) { document.getElementById('fourth_form_filler').setAttribute('style', 'display: none;'); document.getElementById('fourth_form_morning').setAttribute('style', 'display: none;'); document.getElementById('fourth_form_evening').setAttribute('style', 'display: inherit;'); document.getElementsByTagName('textarea')[0].value += 'ВЕЧЕР' + '\n\n' + 'Ведущие: '; }; }; document.getElementById('morning').addEventListener('click', checkMorning); document.getElementById('evening').addEventListener('click', checkEvening);<p>Теперь самое сложное - прикрутить базу данных на <a>IndexedDB</a>. Грубо говоря, мы создадим в браузере пользователя локальное хранилище.</p>
72 // Если выбрано время "утро". function checkMorning() { if (document.getElementById('morning').checked = true) { document.getElementById('fourth_form_filler').setAttribute('style', 'display: none;'); document.getElementById('fourth_form_evening').setAttribute('style', 'display: none;'); document.getElementById('fourth_form_morning').setAttribute('style', 'display: inherit;'); document.getElementsByTagName('textarea')[0].value += 'УТРО' + '\n\n' + 'Ведущие: '; }; }; // Если выбрано время "вечер". function checkEvening() { if (document.getElementById('evening').checked = true) { document.getElementById('fourth_form_filler').setAttribute('style', 'display: none;'); document.getElementById('fourth_form_morning').setAttribute('style', 'display: none;'); document.getElementById('fourth_form_evening').setAttribute('style', 'display: inherit;'); document.getElementsByTagName('textarea')[0].value += 'ВЕЧЕР' + '\n\n' + 'Ведущие: '; }; }; document.getElementById('morning').addEventListener('click', checkMorning); document.getElementById('evening').addEventListener('click', checkEvening);<p>Теперь самое сложное - прикрутить базу данных на <a>IndexedDB</a>. Грубо говоря, мы создадим в браузере пользователя локальное хранилище.</p>
73 <p>Чтобы было удобнее, реализуем эту фичу отдельным модулем - и для начала в самый низ &lt;body&gt; вставляем обращение к файлу speakersDB.js:</p>
73 <p>Чтобы было удобнее, реализуем эту фичу отдельным модулем - и для начала в самый низ &lt;body&gt; вставляем обращение к файлу speakersDB.js:</p>
74 &lt;script src="speakersDB.js"&gt;&lt;/script&gt;<p>Далее работаем в этом файле.</p>
74 &lt;script src="speakersDB.js"&gt;&lt;/script&gt;<p>Далее работаем в этом файле.</p>
75 <p>Вкратце алгоритм работы хранилища на IndexedDB выглядит следующим образом: открыть базу, создать или открыть хранилище объектов (object store) с ключом, совершать с ним транзакции.</p>
75 <p>Вкратце алгоритм работы хранилища на IndexedDB выглядит следующим образом: открыть базу, создать или открыть хранилище объектов (object store) с ключом, совершать с ним транзакции.</p>
76 const dbName = 'База данных гостей эфира'; // Название базы данных. let openRequest = indexedDB.open(dbName, 1); // Открытие базы данных. console.log('Открытие базы данных...');<p>Где "1" - это версия базы.</p>
76 const dbName = 'База данных гостей эфира'; // Название базы данных. let openRequest = indexedDB.open(dbName, 1); // Открытие базы данных. console.log('Открытие базы данных...');<p>Где "1" - это версия базы.</p>
77 <p>У попытки открытия может быть три возможных результата: либо базы ещё нет (и её нужно создать), либо ошибка, либо успех. Отсюда - три разных обработчика.</p>
77 <p>У попытки открытия может быть три возможных результата: либо базы ещё нет (и её нужно создать), либо ошибка, либо успех. Отсюда - три разных обработчика.</p>
78 <p>Если хранилище объектов ещё не создано:</p>
78 <p>Если хранилище объектов ещё не создано:</p>
79 openRequest.onupgradeneeded = function() { let db = openRequest.result; if (!db.objectStoreNames.contains('Гости эфира')) { // Если хранилища 'Гости эфира' не существует... db.createObjectStore('Гости эфира', {keyPath: 'Name'}); // ...создаём хранилище. }; };<p>Если у нас ошибка:</p>
79 openRequest.onupgradeneeded = function() { let db = openRequest.result; if (!db.objectStoreNames.contains('Гости эфира')) { // Если хранилища 'Гости эфира' не существует... db.createObjectStore('Гости эфира', {keyPath: 'Name'}); // ...создаём хранилище. }; };<p>Если у нас ошибка:</p>
80 openRequest.onerror = function() { console.error("Ошибка открытия базы данных.", openRequest.error); };<p>Самая длинная часть - если всё идёт по плану. База данных открыта - сначала ещё немного формальностей.</p>
80 openRequest.onerror = function() { console.error("Ошибка открытия базы данных.", openRequest.error); };<p>Самая длинная часть - если всё идёт по плану. База данных открыта - сначала ещё немного формальностей.</p>
81 openRequest.onsuccess = function() { let db = openRequest.result; console.log('База данных успешно открыта.'); // Защита от повторного открытия вкладки. db.onversionchange = function() { db.close(); alert("База данных устарела, пожалуйста, перезагрузите страницу.") };<p>Дальше нужно записать в базу гостей "по умолчанию", сделать возможность ручного добавления и вывести данные из базы на страницу в блок "Спикеры" (шестая форма).</p>
81 openRequest.onsuccess = function() { let db = openRequest.result; console.log('База данных успешно открыта.'); // Защита от повторного открытия вкладки. db.onversionchange = function() { db.close(); alert("База данных устарела, пожалуйста, перезагрузите страницу.") };<p>Дальше нужно записать в базу гостей "по умолчанию", сделать возможность ручного добавления и вывести данные из базы на страницу в блок "Спикеры" (шестая форма).</p>
82 <p>Начнём с автоматического добавления стартового списка.</p>
82 <p>Начнём с автоматического добавления стартового списка.</p>
83 // Запись в базу данных стартовых значений. // Объявление массива с гостями по умолчанию. let initialGuests = [ { Name: 'Никита Дубко', Post: 'Senior Frontend Developer, Google Developer Expert по Web', Telegram: '@dev_tip' }, { Name: 'Светлана Вронская', Post: 'Эксперт департамента аналитических решений ГК "КОРУС Консалтинг"', Telegram: '@analyticsnow' }, { Name: 'Евгений Некрасов', Post: 'DevOps-инженер кластеров и нейронных сетей', Telegram: '@ravino_doul_channel' }, { Name: 'Роман Душкин', Post: 'Автор и ведущий просветительского YouTube-канала "Душкин объяснит"', Telegram: '@drv_official' } ]; // Добавление гостей по умолчанию в базу, если их ещё там нет. for (let i = 0; i &lt; initialGuests.length; i++) { let transactionWrite = db.transaction('Гости эфира', 'readwrite'); // Создание транзакции. let guests = transactionWrite.objectStore('Гости эфира'); // Получение хранилища объектов 'Гости эфира' для работы с ним. let addInitialGuest = guests.add(initialGuests[i]); // Добавление записи гостя в хранилище объектов. addInitialGuest.onsuccess = function() { console.log('Добавление в базу записи гостя по умолчанию: ' + initialGuests[i].Name); }; addInitialGuest.onerror = function() { console.log('В базе найдены записи по умолчанию.'); }; };<p>Теперь позаботимся о возможности вручную добавить гостя в базу, чтобы продюсер мог ввести Ф. И. О., место работы и Telegram-канал и одним кликом добавить данные в хранилище.</p>
83 // Запись в базу данных стартовых значений. // Объявление массива с гостями по умолчанию. let initialGuests = [ { Name: 'Никита Дубко', Post: 'Senior Frontend Developer, Google Developer Expert по Web', Telegram: '@dev_tip' }, { Name: 'Светлана Вронская', Post: 'Эксперт департамента аналитических решений ГК "КОРУС Консалтинг"', Telegram: '@analyticsnow' }, { Name: 'Евгений Некрасов', Post: 'DevOps-инженер кластеров и нейронных сетей', Telegram: '@ravino_doul_channel' }, { Name: 'Роман Душкин', Post: 'Автор и ведущий просветительского YouTube-канала "Душкин объяснит"', Telegram: '@drv_official' } ]; // Добавление гостей по умолчанию в базу, если их ещё там нет. for (let i = 0; i &lt; initialGuests.length; i++) { let transactionWrite = db.transaction('Гости эфира', 'readwrite'); // Создание транзакции. let guests = transactionWrite.objectStore('Гости эфира'); // Получение хранилища объектов 'Гости эфира' для работы с ним. let addInitialGuest = guests.add(initialGuests[i]); // Добавление записи гостя в хранилище объектов. addInitialGuest.onsuccess = function() { console.log('Добавление в базу записи гостя по умолчанию: ' + initialGuests[i].Name); }; addInitialGuest.onerror = function() { console.log('В базе найдены записи по умолчанию.'); }; };<p>Теперь позаботимся о возможности вручную добавить гостя в базу, чтобы продюсер мог ввести Ф. И. О., место работы и Telegram-канал и одним кликом добавить данные в хранилище.</p>
84 <p>Для этого понадобятся три формы ввода данных, кнопка добавления и привязанная к ней функция сбора введённых значений. Всё перечисленное нужно предварительно создать в памяти, а потом вставить в нужное место страницы.</p>
84 <p>Для этого понадобятся три формы ввода данных, кнопка добавления и привязанная к ней функция сбора введённых значений. Всё перечисленное нужно предварительно создать в памяти, а потом вставить в нужное место страницы.</p>
85 // Вывод формы добавления в базу нового гостя. // Объявление функции добавления нового гостя нажатием на кнопку. function addingGuest() { let transactionWrite2 = db.transaction('Гости эфира', 'readwrite'); // Создание транзакции. let guests2 = transactionWrite2.objectStore('Гости эфира'); // Получение хранилища объектов 'Гости эфира' для работы с ним. let getUserInputName = document.getElementById('newGuestName').value; // Получение введённого пользователем имени нового гостя. let getUserInputPost = document.getElementById('newGuestPost').value; // Получение введённых пользователем должности и места работы нового гостя. let getUserInputTelegram = document.getElementById('newGuestTelegram').value; // Получение введённой пользователем ссылки на Telegram-канал нового гостя. let newGuestFromUser = { Name: getUserInputName, Post: getUserInputPost, Telegram: getUserInputTelegram }; // Добавление записи нового гостя в хранилище объектов. let addNewGuest = guests2.add(newGuestFromUser); addNewGuest.onsuccess = function() { console.log('Добавление в базу нового гостя: ' + newGuestFromUser.Name); }; addNewGuest.onerror = function() { console.log('Ошибка добавления в базу нового гостя.'); }; }; let newGuestNameInput = '&lt;input type="text" id="newGuestName" name="newName" placeholder="Имя гостя" required minlength="5" size="30"&gt;'; let newGuestPostInput = '&lt;input type="text" id="newGuestPost" name="newName" placeholder="Место работы/должность гостя" required minlength="5" size="30"&gt;'; let newGuestTelegramInput = '&lt;input type="text" id="newGuestTelegram" name="newName" placeholder="Telegram-канал гостя (если есть)" minlength="5" size="30"&gt;&lt;br&gt;'; let newGuestNameLabel = '&lt;label for="newGuestName"&gt;Добавить гостя в базу:&lt;/label&gt;&lt;br&gt;'; let newGuestButton = document.createElement('input'); newGuestButton.setAttribute('type', 'button'); newGuestButton.setAttribute('value', 'Добавить'); newGuestButton.addEventListener('click', addingGuest); let path2 = document.getElementById('sixth_form').getElementsByTagName('fieldset')[0]; path2.insertAdjacentHTML('beforeend', newGuestNameLabel); path2.insertAdjacentHTML('beforeend', newGuestNameInput); path2.insertAdjacentHTML('beforeend', newGuestPostInput); path2.insertAdjacentHTML('beforeend', newGuestTelegramInput); path2.append(newGuestButton); path2.insertAdjacentHTML('beforeend', '&lt;br&gt;&lt;br&gt;Гости в базе:&lt;br&gt;');<p>Наконец, уже существующих в базе спикеров нужно вывести на страницу и предложить для выбора. Это другая транзакция, и делается она следующим образом.</p>
85 // Вывод формы добавления в базу нового гостя. // Объявление функции добавления нового гостя нажатием на кнопку. function addingGuest() { let transactionWrite2 = db.transaction('Гости эфира', 'readwrite'); // Создание транзакции. let guests2 = transactionWrite2.objectStore('Гости эфира'); // Получение хранилища объектов 'Гости эфира' для работы с ним. let getUserInputName = document.getElementById('newGuestName').value; // Получение введённого пользователем имени нового гостя. let getUserInputPost = document.getElementById('newGuestPost').value; // Получение введённых пользователем должности и места работы нового гостя. let getUserInputTelegram = document.getElementById('newGuestTelegram').value; // Получение введённой пользователем ссылки на Telegram-канал нового гостя. let newGuestFromUser = { Name: getUserInputName, Post: getUserInputPost, Telegram: getUserInputTelegram }; // Добавление записи нового гостя в хранилище объектов. let addNewGuest = guests2.add(newGuestFromUser); addNewGuest.onsuccess = function() { console.log('Добавление в базу нового гостя: ' + newGuestFromUser.Name); }; addNewGuest.onerror = function() { console.log('Ошибка добавления в базу нового гостя.'); }; }; let newGuestNameInput = '&lt;input type="text" id="newGuestName" name="newName" placeholder="Имя гостя" required minlength="5" size="30"&gt;'; let newGuestPostInput = '&lt;input type="text" id="newGuestPost" name="newName" placeholder="Место работы/должность гостя" required minlength="5" size="30"&gt;'; let newGuestTelegramInput = '&lt;input type="text" id="newGuestTelegram" name="newName" placeholder="Telegram-канал гостя (если есть)" minlength="5" size="30"&gt;&lt;br&gt;'; let newGuestNameLabel = '&lt;label for="newGuestName"&gt;Добавить гостя в базу:&lt;/label&gt;&lt;br&gt;'; let newGuestButton = document.createElement('input'); newGuestButton.setAttribute('type', 'button'); newGuestButton.setAttribute('value', 'Добавить'); newGuestButton.addEventListener('click', addingGuest); let path2 = document.getElementById('sixth_form').getElementsByTagName('fieldset')[0]; path2.insertAdjacentHTML('beforeend', newGuestNameLabel); path2.insertAdjacentHTML('beforeend', newGuestNameInput); path2.insertAdjacentHTML('beforeend', newGuestPostInput); path2.insertAdjacentHTML('beforeend', newGuestTelegramInput); path2.append(newGuestButton); path2.insertAdjacentHTML('beforeend', '&lt;br&gt;&lt;br&gt;Гости в базе:&lt;br&gt;');<p>Наконец, уже существующих в базе спикеров нужно вывести на страницу и предложить для выбора. Это другая транзакция, и делается она следующим образом.</p>
86 // Вывод на страницу гостей из базы данных. let transactionRead = db.transaction('Гости эфира', 'readonly'); // Создание транзакции. let existingGuests = transactionRead.objectStore('Гости эфира'); // Получение хранилища объектов 'Гости эфира' для работы с ним. let readRequest = existingGuests.getAll(); readRequest.onsuccess = function(e) { console.log('ЧТЕНИЕ БАЗЫ ДАННЫХ'); console.log('В базе найдено ' + readRequest.result.length + ' гостей.'); // Перебор гостей в базе для уведомления в консоль и вывода на страницу. for (let eg = 0; eg &lt; readRequest.result.length; eg++) { let printName = readRequest.result[eg].Name; // Получение Ф. И. О. let printPost = readRequest.result[eg].Post; // Получение должности и места работы. let printTelegram = readRequest.result[eg].Telegram; // Получение ссылки на Telegram-канал. // Финальная строка с данными гостя (для вывода пользователю). let printGuest = printName + ', ' + printPost + ', ' + printTelegram; // Уведомление в консоль о найденном в базе пользователе. console.log('В базе найден гость ' + printGuest); // Вставка списка на страницу. let guestInput = document.createElement('input'); guestInput.type = 'checkbox'; guestInput.id = 'guest' + eg; guestInput.name = 'air_guest'; let guestLabel = document.createElement('label'); guestLabel.setAttribute('for', ['guest' + eg]); guestLabel.innerHTML = printGuest; // Добавление переноса строки после каждого гостя. let newLine = document.createElement('br'); let path = document.getElementById('sixth_form').getElementsByTagName('fieldset')[0]; path.append(guestInput); path.append(guestLabel); guestLabel.after(newLine); // Вставка функции выбора гостей к каждому пункту с именами гостей. for (g = 0; g &lt; document.getElementsByName('air_guest').length; g++) { document.getElementsByName('air_guest')[g].addEventListener('click', checkGuest); }; }; }; };<p>На этом реализация закончена.</p>
86 // Вывод на страницу гостей из базы данных. let transactionRead = db.transaction('Гости эфира', 'readonly'); // Создание транзакции. let existingGuests = transactionRead.objectStore('Гости эфира'); // Получение хранилища объектов 'Гости эфира' для работы с ним. let readRequest = existingGuests.getAll(); readRequest.onsuccess = function(e) { console.log('ЧТЕНИЕ БАЗЫ ДАННЫХ'); console.log('В базе найдено ' + readRequest.result.length + ' гостей.'); // Перебор гостей в базе для уведомления в консоль и вывода на страницу. for (let eg = 0; eg &lt; readRequest.result.length; eg++) { let printName = readRequest.result[eg].Name; // Получение Ф. И. О. let printPost = readRequest.result[eg].Post; // Получение должности и места работы. let printTelegram = readRequest.result[eg].Telegram; // Получение ссылки на Telegram-канал. // Финальная строка с данными гостя (для вывода пользователю). let printGuest = printName + ', ' + printPost + ', ' + printTelegram; // Уведомление в консоль о найденном в базе пользователе. console.log('В базе найден гость ' + printGuest); // Вставка списка на страницу. let guestInput = document.createElement('input'); guestInput.type = 'checkbox'; guestInput.id = 'guest' + eg; guestInput.name = 'air_guest'; let guestLabel = document.createElement('label'); guestLabel.setAttribute('for', ['guest' + eg]); guestLabel.innerHTML = printGuest; // Добавление переноса строки после каждого гостя. let newLine = document.createElement('br'); let path = document.getElementById('sixth_form').getElementsByTagName('fieldset')[0]; path.append(guestInput); path.append(guestLabel); guestLabel.after(newLine); // Вставка функции выбора гостей к каждому пункту с именами гостей. for (g = 0; g &lt; document.getElementsByName('air_guest').length; g++) { document.getElementsByName('air_guest')[g].addEventListener('click', checkGuest); }; }; }; };<p>На этом реализация закончена.</p>
87 <p>Убедиться в правильности работы хранилища можно двумя способами.</p>
87 <p>Убедиться в правильности работы хранилища можно двумя способами.</p>
88 <p>Во-первых, для этой цели у нас предусмотрены консольные уведомления о работе локального хранилища.</p>
88 <p>Во-первых, для этой цели у нас предусмотрены консольные уведомления о работе локального хранилища.</p>
89 Консольные уведомления о работе локального хранилища на IndexedDB (вид в Mozilla Firefox)<em>Скриншот: Skillbox Media</em><p>Во-вторых, на него можно взглянуть в браузерных инструментах разработчика на вкладке Приложение → Хранилище → IndexedDB (в Google Chrome) или Хранилище → IndexedDB (в Mozilla Firefox).</p>
89 Консольные уведомления о работе локального хранилища на IndexedDB (вид в Mozilla Firefox)<em>Скриншот: Skillbox Media</em><p>Во-вторых, на него можно взглянуть в браузерных инструментах разработчика на вкладке Приложение → Хранилище → IndexedDB (в Google Chrome) или Хранилище → IndexedDB (в Mozilla Firefox).</p>
90 Просмотр созданного локального хранилища в Google Chrome<em>Скриншот: Skillbox Media</em>Просмотр созданного локального хранилища в Mozilla Firefox<em>Скриншот: личный архив Евгения Колесникова</em><p>Наш эксперимент увенчался успехом, но это лишь первая бета-версия с самыми важными функциями. И к ней всегда можно добавить что-то ещё. Однако важнее другое: мы не использовали никаких фреймворков и сложных "продвинутых" языков. То есть для такого приложения достаточно относительно простых и известных каждому веб-разработчику инструментов (если не считать IndexedDB). А экономия времени получается колоссальная.</p>
90 Просмотр созданного локального хранилища в Google Chrome<em>Скриншот: Skillbox Media</em>Просмотр созданного локального хранилища в Mozilla Firefox<em>Скриншот: личный архив Евгения Колесникова</em><p>Наш эксперимент увенчался успехом, но это лишь первая бета-версия с самыми важными функциями. И к ней всегда можно добавить что-то ещё. Однако важнее другое: мы не использовали никаких фреймворков и сложных "продвинутых" языков. То есть для такого приложения достаточно относительно простых и известных каждому веб-разработчику инструментов (если не считать IndexedDB). А экономия времени получается колоссальная.</p>
91 <a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>
91 <a><b>Бесплатный курс по Python ➞</b>Мини-курс для новичков и для опытных кодеров. 4 крутых проекта в портфолио, живое общение со спикером. Кликните и узнайте, чему можно научиться на курсе. Смотреть программу</a>