Пишем клиент для Голос в связке с CMS Joomla. Часть 2
И снова здравствуйте. На связи @captain. Продолжаем пошаговое написание web-клиента голоса. Это вторая часть( Часть 1 ) и в ней мы разберемся как сверстать основной экран клиента и как вывести карточки постов на него.
Я вижу для себя это вот таким образом:
Слева идет лента постов, которую мы прокручиваем колесиком мышки. При выборе поста он загружается в соседнюю правую область и мы можем прочитать его, проголосовать за него, оставить комментарий и т.п.
Для Bootstrap наша разметка будет выглядеть вот так:
<div class="row">
<div class="col-lg-5">
<nav class="">
<div class="collapse navbar-collapse" id="items_collapsable_wrapper">
<div id="items_list_wrapper" class="row">
</div>
</div>
</nav>
</div>
<div class="col-lg-7">
<?php require_once(JPATH_ROOT . '/components/com_q/views/item.php'); ?>
</div>
</div>
Код на pastebin
Внимательный читатель обратил внимание, что в правый div у нас подгружается файл item.php, который отвечает за вывод самого поста. Его мы будем разбирать в следующей части.
Сейчас же я лишь отмечу, что используется элемент navbar за счет чего прокрутка области с карточками постов ограничивается пределами экрана. Таким образом мы прокручиваем только ленту постов, открытый пост прокручивается отдельно.
Я неспроста выбрал такой механизм. Используя его, очень просто знакомиться с лентой постов. Достаточно нажать на пост и и он отображается справа, его значительную часть можно увидеть сразу и решить заслуживает ли он внимания или нет. И так же просто можно перейти к следующему посту, так как ничего "закрывать" не надо. Минимизируем и клики мышки и ожидание.
Ну что же, к делу. Для начала нужно определиться какие именно данные мы хотим видеть в ленте. Давайте сделаем так, чтобы пользователь видел новые посты. Поможет нам в этом такая функция:
function getDiscussions()
{
var params =
{
'limit': 100,
'truncate_body': 240
}
golos.api.getDiscussionsByCreated(params, function(err, data){
if(data.length > 0)
{
data.forEach(function (operation){
AddBlockX(operation);
});
}
});
}
Код на pastebin
Здесь мы получаем последние 100 постов и 240 символов текста каждого из них. Больше 100 постов за раз API нам не отдаст, да нам и не надо больше. Так как у нас будет несколько различных вариантов заполнения ленты, то логично вывести функцию упаковки и вывода карточки поста в отдельную функцию AddBlockX
.
Чуть ниже мы рассмотрим ее подробно. Пока скажу, что нам понадобятся некоторые вспомогательные вещи, например библиотека moment.js, которая классно умеет форматировать даты. Мы используем ее так:
function getCommentDate(adate)
{
var date = new Date(adate);
var offset = date.getTimezoneOffset();
date.setMinutes(date.getMinutes() - offset);
return moment(date, "YYYYMMDD").fromNow();
}
Код на pastebin
В результате вместо вывода обычной даты вернется "час назад" или "20 минут назад". Тут же учитывается, что время нод голоса отличается от локального времени пользователя, например от московского времени date.getTimezoneOffset()
.
Еще один момент. Нам придется работать с тэгами. При портировании стима, почему-то решили, что в эпоху многобайтовых кодировок, лучше использовать транслитерацию для кириллицы. Как по мне, то странное решение, но имеем то что имеем. Поэтому из исходников клиента tolstoy мы стащим вот такой кусок:
var d = /\s+/g,
rus = "щ ш ч ц й ё э ю я х ж а б в г д е з и к л м н о п р с т у ф ъ ы ь".split(d),
eng = "shch sh ch cz ij yo ye yu ya kh zh a b v g d e z i k l m n o p r s t u f xx y x".split(d);
function detransliterate(str, reverse) {
if (!reverse && str.substring(0, 4) !== 'ru--') return str
if (!reverse) str = str.substring(4)
// TODO rework this
// (didnt placed this earlier because something is breaking and i am too lazy to figure it out ;( )
if(!reverse) {
// str = str.replace(/j/g, 'ь')
// str = str.replace(/w/g, 'ъ')
str = str.replace(/yie/g, 'ые')
}
else {
// str = str.replace(/ь/g, 'j')
// str = str.replace(/ъ/g, 'w')
str = str.replace(/ые/g, 'yie')
}
var i,
s = /[^[\]]+(?=])/g, orig = str.match(s),
t = /<(.|\n)*?>/g, tags = str.match(t);
if(reverse) {
for(i = 0; i < rus.length; ++i) {
str = str.split(rus[i]).join(eng[i]);
str = str.split(rus[i].toUpperCase()).join(eng[i].toUpperCase());
}
}
else {
for(i = 0; i < rus.length; ++i) {
str = str.split(eng[i]).join(rus[i]);
str = str.split(eng[i].toUpperCase()).join(rus[i].toUpperCase());
}
}
if(orig) {
var restoreOrig = str.match(s);
for (i = 0; i < restoreOrig.length; ++i)
str = str.replace(restoreOrig[i], orig[i]);
}
if(tags) {
var restoreTags = str.match(t);
for (i = 0; i < restoreTags.length; ++i)
str = str.replace(restoreTags[i], tags[i]);
str = str.replace(/\[/g, '').replace(/\]/g, '');
}
return str;
}
Код на pastebin
В принципе понятно, что он задает правила прямой и обратной транслитерации. Заострять внимание на нем не будем.
А вот и функция, которая формирует карточку поста. Разберем ее подробнее ниже.
function AddBlockX(operation)
{
var listWrapper = document.getElementById('items_list_wrapper');
var main_div = document.createElement("div");
main_div.classList.add("col-xs-12","q_wrapper");
var metadata = JSON.parse(operation.json_metadata);
if(metadata.image)
{
var image = metadata.image[0];
}
else
{
var image = '/components/com_q/noimage.png';
}
var img_div = document.createElement("div");
img_div.classList.add("img_div");
main_div.appendChild(img_div);
img_div.style.backgroundImage = "url('"+image+"')";
var q_div = document.createElement("div");
q_div.classList.add("q_div");
main_div.appendChild(q_div);
var title = operation.title;
var author = operation.author;
var created = operation.created;
var last_update = operation.last_update;
var total_payout_value = operation.total_payout_value;
var pending_payout_value = operation.pending_payout_value;
var total_pending_payout_value = operation.total_pending_payout_value;
var votes = operation.active_votes.length;
var vl = total_pending_payout_value;
if(total_payout_value > total_pending_payout_value)
{
vl = total_payout_value;
}
var tags = '';
if(typeof metadata.tags !== "undefined")
{
var tags_count = metadata.tags.length;
for(var i = 0;i < tags_count;i++)
{
if(tags_count > 1)
{
tags = tags + " <span class='label label-warning'><a href='/created/" + metadata.tags[i] + "'>"+detransliterate(metadata.tags[i], 0)+'</a></span>';
}
}
}
var dt = getCommentDate(created);
var s = 'onClick="getContentX(\''+operation.permlink.trim() +'\', \''+operation.author.trim() +'\');"';
q_div.innerHTML = '<div class="q_header_wrapper"><h3><a href="javascript:void(0)" '+s+'>'+ title + '</a></h3></div>' + dt +' - Автор: <a href="/@'+ author +'" title="Все посты пользователя">@' + author + '</a>' + '<br/> Голосов <strong>' + votes + '</strong> на сумму <strong>' + vl + '</strong><br/>' + tags;
var clearFix = document.createElement("div");
clearFix.classList.add("clearFix");
main_div.appendChild(clearFix);
listWrapper.appendChild(main_div);
}
Код на pastebin
Пройдемся по функции чуть подробнее. Сначала мы создаем div в котором будет все располагаться. Потом получаем метаданные var metadata = JSON.parse(operation.json_metadata);
Тут содержатся данные о ссылках и картинках поста, клиенте с которого он был опубликован и т.п. Нас интересует картинка, которую можно было бы вывести в карточке поста. Если она есть if(metadata.image)
, то выведем ее, иначе выводим заглушку. Картинку выводим как бэкграунд img_div.style.backgroundImage = "url('"+image+"')";
Это позволит выводить анимированные gif, а также центрирует и масштабирует картинку (это мы задаем в css).
Дальше вытаскиваем различные данные поста такие как заголовок, автор, дата создания, выплаты и т.п. Тут же можно определить количество голосов за пост var votes = operation.active_votes.length;
.
Так же в метаданных поста хранятся его тэги. Технически пост должен иметь хотя бы один тэг. Первый тэг принято использовать в качестве категории, но я эти убеждения не разделяю, поэтому в моем случае все тэги равноправны. И мы создаем их список со ссылками, чтобы при клике по тэгу можно было бы выбрать все соответствующие посты с данным тэгом:
if(typeof metadata.tags !== "undefined")
{
var tags_count = metadata.tags.length;
for(var i = 0;i < tags_count;i++)
{
if(tags_count > 1)
{
tags = tags + " <span class='label label-warning'><a href='/created/" + metadata.tags[i] + "'>"+detransliterate(metadata.tags[i], 0)+'</a></span>';
}
}
}
Тут нужно отметить еще такой любопытный момент, что клиент golos.io выдает ссылки в своем формате и нам нужно этого формата придерживаться для совместимости с другими клиентами. Поэтому ссылка на вывод всех постов по тэгу tag будет иметь вид: /created/tag/
, а для тэга хобот будет /created/ru--khobot/
.
У нас все готово. Объединяем все наши данные для вывода в карточку:
var s = 'onClick="getContentX(\''+operation.permlink.trim() +'\', \''+operation.author.trim() +'\');"';
q_div.innerHTML = '<div class="q_header_wrapper"><h3><a href="javascript:void(0)" '+s+'>'+ title + '</a></h3></div>' + dt +' - Автор: <a href="/@'+ author +'" title="Все посты пользователя">@' + author + '</a>' + '<br/> Голосов <strong>' + votes + '</strong> на сумму <strong>' + vl + '</strong><br/>' + tags;
Имя автора так же делаем ссылкой, по которой открывается страница автора со всеми его постами. А заголовок статьи, ссылка, которая вызывает функцию загрузки контента getContentX
в соседний блок для просмотра.
Если мы хотим выводить посты определенного автора, то нам поможет вот такая функция:
function getDiscussionsByAuthor(author)
{
document.getElementById('loader').style = 'display:block';
var params =
{
'limit': 100,
'truncate_body': 240,
'select_authors': [author]
}
golos.api.getDiscussionsByCreated(params, function(err, data){
if(data.length > 0)
{
data.forEach(function (operation){
AddBlockX(operation);
});
}
});
}
Код на pastebin
а если посты по определенному тэгу, то вот такая:
function getDiscussionsByTags(tags)
{
var params =
{
'limit': 50,
'select_tags': tags,
'truncate_body': 240
}
golos.api.getDiscussionsByCreated(params, function(err, data){
data.sort(compareDate);
if(data.length > 0)
{
data.forEach(function (operation){
AddBlockX(operation);
});
}
});
}
Код на pastebin
У нас здесь используется функция для сортировки массива по времени, она очень простая:
function compareDate(a, b)
{
if(a.created > b.created)
{
return -1;
}
else{
return 1;
}
}
Код на pastebin
Нужна она потому, что мы можем указать несколько тэгов через запятую. А вот посты по ним вернутся к нам вразнобой. И нам самим приходится их упорядочивать.
Это практически все, что мы будем использовать для вывода ленты постов. Будет еще вывод ленты пользователя, но о нем поговорим, когда доберемся до страницы пользователя.
У нас осталось только одно затруднение - ограничение на 100 возвращаемых постов. После прокрутки ленты хорошо бы сделать кнопочку "Дальше" и подгрузить следующие 100 постов. Так в чем же проблема? За дело.
В этом нам помогут параметры start_author и start_permlink для методов getDiscussions*
.
Перепишем код следующим образом:
var startAuthor;
var startPermlink;
function getDiscussions(start_author, start_permlink)
{
start_author = typeof start_author !== 'undefined' ? start_author : '';
start_permlink = typeof start_permlink !== 'undefined' ? start_permlink : '';
if(start_permlink && start_author)
{
var params =
{
'limit': 100,
'truncate_body': 240,
'start_author': start_author,
'start_permlink': start_permlink
}
}
else
{
var params =
{
'limit': 100,
'truncate_body': 240
}
}
golos.api.getDiscussionsByCreated(params, function(err, data){
if(data.length > 0)
{
data.forEach(function (operation){
AddBlockX(operation);
});
}
});
}
function AddBlockX(operation)
{
...
startAuthor = operation.author;
startPermlink = operation.permlink;
...
}
Код на pastebin
А в конец ленты постов добавим кнопку
<button type='button' onClick='getDiscussions(startAuthor, startPermlink);'>Дальше</button>
Вот теперь у нас получилась полноценная навигация. Думаю что остальные методы из группы getDiscussions*
вы адаптируете и без моей помощи.
Пока что все. В следующей статье цикла будем показывать пост, комментарии и все что к этому относится.
И немного пиара. @captain это делегат голоса. Вот его делегатский пост https://golos.io/@captain/post-delegata-captain/
@captain много чего делает для голоса и много чего еще сделает. Поэтому не поленитесь и проголосуйте за него тут https://golos.io/~witnesses или тут https://goldvoice.club/witnesses/