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>элементарная сетка из трёх <div>-блоков для разделения страницы на три вертикальных уровня (<div id="firstBlock">, <div id="secondBlock">, <div id="thirdBlock">);</li>
46
<ul><li>элементарная сетка из трёх <div>-блоков для разделения страницы на три вертикальных уровня (<div id="firstBlock">, <div id="secondBlock">, <div id="thirdBlock">);</li>
47
<li>первый уровень (см. скриншот) - заголовок "Расписание эфира" и кнопка отправки на синем фоне;</li>
47
<li>первый уровень (см. скриншот) - заголовок "Расписание эфира" и кнопка отправки на синем фоне;</li>
48
<li>второй уровень - блоки "Дата эфира", "Вид эфира", "Ведущие";</li>
48
<li>второй уровень - блоки "Дата эфира", "Вид эфира", "Ведущие";</li>
49
<li>третий уровень - блоки "Время подключения", "Предпросмотр поста", "Спикеры";</li>
49
<li>третий уровень - блоки "Время подключения", "Предпросмотр поста", "Спикеры";</li>
50
<li>CSS-правил будет немного, поэтому разместим их не в отдельном файле, а внутри страницы с помощью элемента <style>.</li>
50
<li>CSS-правил будет немного, поэтому разместим их не в отдельном файле, а внутри страницы с помощью элемента <style>.</li>
51
</ul><p>С опциями интерфейса в целом всё тоже просто: чтобы дать пользователю возможность выбора пунктов, вставим в наши <div>-блоки шесть HTML-форм (элементы <form>), внутри которых будут связанные элементы <input>/<label>, <fieldset>/<legend> или <textarea>.</p>
51
</ul><p>С опциями интерфейса в целом всё тоже просто: чтобы дать пользователю возможность выбора пунктов, вставим в наши <div>-блоки шесть HTML-форм (элементы <form>), внутри которых будут связанные элементы <input>/<label>, <fieldset>/<legend> или <textarea>.</p>
52
<p>Взаимодействие форм с инпутами и подписями поначалу может показаться запутанным, поэтому разберём его подробнее:</p>
52
<p>Взаимодействие форм с инпутами и подписями поначалу может показаться запутанным, поэтому разберём его подробнее:</p>
53
<ul><li>форма (элемент<a><form></a>) - это секция документа, содержащая интерактивные элементы для отправки информации. Содержит элементы формы (см. ниже);</li>
53
<ul><li>форма (элемент<a><form></a>) - это секция документа, содержащая интерактивные элементы для отправки информации. Содержит элементы формы (см. ниже);</li>
54
<li>инпут (элемент<a><input></a>) - это как раз элемент управления одного из нескольких возможных типов. Мы будем использовать инпуты типа radio (выбор только одного варианта) и checkbox (выбор любого количества вариантов);</li>
54
<li>инпут (элемент<a><input></a>) - это как раз элемент управления одного из нескольких возможных типов. Мы будем использовать инпуты типа radio (выбор только одного варианта) и checkbox (выбор любого количества вариантов);</li>
55
<li>элемент<a><label></a> - это подпись для связанного инпута. Связь указывается с помощью атрибутов id и for;</li>
55
<li>элемент<a><label></a> - это подпись для связанного инпута. Связь указывается с помощью атрибутов id и for;</li>
56
<li>элемент<a><fieldset></a>группирует несколько управляющих элементов формы, а связанный с ним<a><legend></a>буквально добавляет над ними "легенду" (красивый заголовок).</li>
56
<li>элемент<a><fieldset></a>группирует несколько управляющих элементов формы, а связанный с ним<a><legend></a>буквально добавляет над ними "легенду" (красивый заголовок).</li>
57
</ul><p>Мы нарушим эту схему только в двух случаях: для кнопки отправки в Telegram укажем тип submit ("отправка", без подписи с помощью <label>), а форма предпросмотра поста будет содержать только текстовый блок <textarea> для расписания.</p>
57
</ul><p>Мы нарушим эту схему только в двух случаях: для кнопки отправки в Telegram укажем тип submit ("отправка", без подписи с помощью <label>), а форма предпросмотра поста будет содержать только текстовый блок <textarea> для расписания.</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>Отдельные настройки области для текста (элемент <textarea> в пятой форме). В частности, убираем возможность менять её размер (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>Отдельные настройки области для текста (элемент <textarea> в пятой форме). В частности, убираем возможность менять её размер (resize: none) и добавляем возможность прокрутки на случай, если текста будет много (ну мало ли).</p>
65
#fifth_form > fieldset { overflow: scroll; } textarea { width: 95%; height: 92%; padding: 2%; font-size: 18px; resize: none; border: 0; }<p>Оставшиеся CSS-правила не так важны - и вы сможете увидеть их в финальном варианте. А мы пойдём к самому интересному - программированию поведения элементов.</p>
65
#fifth_form > 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-страницы - в элемент <head>:</p>
67
<p>Этот блок мы добавим ближе к началу кода HTML-страницы - в элемент <head>:</p>
68
<script> // Функция отправки расписания в 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 + '&parse_mode=HTML&text=' + text; fetch(sendURL); alert('Расписание отправлено.'); console.log('Отправка расписания в Telegram-чат.'); }; // Функция отмены предварительного выбора пунктов в меню. function uncheckInputs() { var inputs = document.getElementsByTagName('input'); for (var i = 0; i < inputs.length; i++) { inputs[i].checked = false; }; }; </script><p>Остановимся ненадолго на реализации отправки поста в Telegram (функция sendSchedule). Нужно всего лишь сделать GET-запрос методом<a>sendMessage()</a>из Telegram Bot API с помощью JavaScript-метода<a>fetch()</a>. Требуется только подставить в нужные переменные токен вашего бота (его можно получить при создании бота у BotFather) и ID чата.</p>
68
<script> // Функция отправки расписания в 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 + '&parse_mode=HTML&text=' + text; fetch(sendURL); alert('Расписание отправлено.'); console.log('Отправка расписания в Telegram-чат.'); }; // Функция отмены предварительного выбора пунктов в меню. function uncheckInputs() { var inputs = document.getElementsByTagName('input'); for (var i = 0; i < inputs.length; i++) { inputs[i].checked = false; }; }; </script><p>Остановимся ненадолго на реализации отправки поста в Telegram (функция sendSchedule). Нужно всего лишь сделать GET-запрос методом<a>sendMessage()</a>из Telegram Bot API с помощью JavaScript-метода<a>fetch()</a>. Требуется только подставить в нужные переменные токен вашего бота (его можно получить при создании бота у BotFather) и ID чата.</p>
69
-
<p>Далее конст��уктор собирает из переменных ссылку для запроса, а чтобы функция срабатывала по клику на кнопку отправки, мы добавляем кнопке обработчик с названием функции:</p>
69
+
<p>Далее конструктор собирает из переменных ссылку для запроса, а чтобы функция срабатывала по клику на кнопку отправки, мы добавляем кнопке обработчик с названием функции:</p>
70
<input type="submit" value="Отправить расписание" id="sendButton" onclick="sendSchedule()"><p>Ещё один скрипт - после отрисовки первой формы "Дата эфира", для которой требуется получить сегодняшнюю и завтрашнюю даты.</p>
70
<input type="submit" value="Отправить расписание" id="sendButton" onclick="sendSchedule()"><p>Ещё один скрипт - после отрисовки первой формы "Дата эфира", для которой требуется получить сегодняшнюю и завтрашнюю даты.</p>
71
<script> // Определяем и вставляем актуальные даты (сегодня и завтра). 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; </script><p>В самый конец тела страницы вставляем вызовы функции отмены предварительного выбора пунктов меню uncheckInputs() и функций - обработчиков кликов по пунктам меню. Приведём часть этого блока - остальное будет по тому же принципу.</p>
71
<script> // Определяем и вставляем актуальные даты (сегодня и завтра). 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; </script><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>Чтобы было удобнее, реализуем эту фичу отдельным модулем - и для начала в самый низ <body> вставляем обращение к файлу speakersDB.js:</p>
73
<p>Чтобы было удобнее, реализуем эту фичу отдельным модулем - и для начала в самый низ <body> вставляем обращение к файлу speakersDB.js:</p>
74
<script src="speakersDB.js"></script><p>Далее работаем в этом файле.</p>
74
<script src="speakersDB.js"></script><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 < 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 < 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 = '<input type="text" id="newGuestName" name="newName" placeholder="Имя гостя" required minlength="5" size="30">'; let newGuestPostInput = '<input type="text" id="newGuestPost" name="newName" placeholder="Место работы/должность гостя" required minlength="5" size="30">'; let newGuestTelegramInput = '<input type="text" id="newGuestTelegram" name="newName" placeholder="Telegram-канал гостя (если есть)" minlength="5" size="30"><br>'; let newGuestNameLabel = '<label for="newGuestName">Добавить гостя в базу:</label><br>'; 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', '<br><br>Гости в базе:<br>');<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 = '<input type="text" id="newGuestName" name="newName" placeholder="Имя гостя" required minlength="5" size="30">'; let newGuestPostInput = '<input type="text" id="newGuestPost" name="newName" placeholder="Место работы/должность гостя" required minlength="5" size="30">'; let newGuestTelegramInput = '<input type="text" id="newGuestTelegram" name="newName" placeholder="Telegram-канал гостя (если есть)" minlength="5" size="30"><br>'; let newGuestNameLabel = '<label for="newGuestName">Добавить гостя в базу:</label><br>'; 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', '<br><br>Гости в базе:<br>');<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 < 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 < 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 < 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 < 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>