JSFiddle - React, Tailwind, and code Playground

HTML

<script src="http://underscorejs.org/underscore.js"></script>
<script src="http://momentjs.com/downloads/moment-with-locales.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/ractive"></script>
<main>
<div style="width:600px" id="comments"></div>
</main>
<script id="comments_template" type="ractive/text">
   {{#top_reply}}
    <!--Мы показываем форму добавления комментария сверху и снизу. Но когда комментариев нет, только снизу-->
    {{#if replys.length > 0}}
        <!--Если есть черновик - покажем форму-->
        {{#if this.reply_draft}}
            <!--это развернется в темплейт который находится между блоками comment_form - темплейт формы введения комментария.-->
            {{>comment_form}}
        {{else}}
            <!--Если нет, то покажем кнопку 'Добавить комментарий' которая раскроет форму -->
            <!--По нажатию выстрелит событие reply и e.context = this; this = top_reply            -->
            <button class="add_comment" on-click="reply">Добавить комментарий</button>
        {{/if}}
    {{/if}}
{{/top_reply}}

<!--Для каждого комментария верхнего уровня рендерим partial комментария смотри ниже-->
{{#replys}}
    {{>comment}}
{{/replys}}

<!--Нижняя форма ответа-->
{{#bottom_reply}}
    {{>comment_form}}
{{/bottom_reply}}


<!-- {{>comment}} -->
<!--Темплейт комментария -->
<!--Важно понять что поскольку этот partial показываем из блока replys - this будет равняться текущему комментарию-->
<article id="{{this.id}}" class="comment" intro="scroll_to:{go:{{this.is_new}}}">
    <header>
        <!--Тут мы выставляем пользовательский аватар либо дефолтную картинку если у пользователя нет аватара-->
        <img class="avatar" src="{{this.user.image || default_avatar_url}}"/>

        <span class="author">{{this.user.username}}</span>
        <!-- Можно использовать функции. Здесь мы используем moment что бы отформатировать время. Ссылку на функцию мы...

CSS

.fotorama__arr:focus:after,.fotorama__fullscreen-icon:focus:after,.fotorama__html,.fotorama__img,.fotorama__nav__frame:focus .fotorama__dot:after,.fotorama__nav__frame:focus .fotorama__thumb:after,.fotorama__stage__frame,.fotorama__stage__shaft,.fotorama__video iframe{position:absolute;width:100%;height:100%;top:0;right:0;left:0;bottom:0}.fotorama--fullscreen,.fotorama__img{max-width:99999px!important;max-height:99999px!important;min-width:0!important;min-height:0!important;border-radius:0!important;box-shadow:none!important;padding:0!important}.fotorama__wrap .fotorama__grab{cursor:-webkit-grab;cursor:-o-grab;cursor:-ms-grab;cursor:grab}.fotorama__grabbing *{cursor:-webkit-grabbing;cursor:-o-grabbing;cursor:-ms-grabbing;cursor:grabbing}.fotorama__spinner{position:absolute!important;top:50%!important;left:50%!important}.fotorama__wrap--css3 .fotorama__arr,.fotorama__wrap--css3 .fotorama__fullscreen-icon,.fotorama__wrap--css3 .fotorama__nav__shaft,.fotorama__wrap--css3 .fotorama__stage__shaft,.fotorama__wrap--css3 .fotorama__thumb-border,.fotorama__wrap--css3 .fotorama__video-play{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.fotorama__caption,.fotorama__nav:after,.fotorama__nav:before,.fotorama__stage:after,.fotorama__stage:before,.fotorama__wrap--css3 .fotorama__html,.fotorama__wrap--css3 .fotorama__nav,.fotorama__wrap--css3 .fotorama__spinner,.fotorama__wrap--css3 .fotorama__stage,.fotorama__wrap--css3 .fotorama__stage .fotorama__img,.fotorama__wrap--css3 .fotorama__stage__frame{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.fotorama__arr:focus,.fotorama__fullscreen-icon:focus,.fotorama__nav__frame{outline:0}.fotorama__arr:focus:after,.fotorama__fullscreen-icon:focus:after,.fotorama__nav__frame:focus .fotorama__dot:after,.fotorama__nav__frame:focus .fotorama__thumb:after{content:'';border-radius:inherit;background-color:rgba(0,175,234,.5)}.fotorama__wrap--video .fotorama__stage,.fotorama__wrap--video...

JavaScript

function Comment(id, md_text, user, domain, date, reply_to) {

	this.id = id;
	this.md_text = md_text;
	this.user = user;
	this.date = date;
	this.reply_to = reply_to ? reply_to.id || reply_to : null;
	this.domain = domain;
	this.replys = [];
};

Comment.prototype.destroy = function () {
	return $('<div></div>')
		.promise()
		.then(function () {
			return {
				md_text: '*Комментарий был удален*'
			}
		});
};

Comment.prototype.save = function () {
	var self = this;
	return $('<div></div>')
		.promise()
		.then(function (rep) {
            
                self.id = Math.random();
			    self.is_new = true;
           
			return self;
		});
};

Comment.fetch = function (domain, options) {
	return $('<div></div>')
		.promise()
		.then(function (res) {
			return [];
		});
};

var Comments = Ractive.extend({
	//$(selector) Элемента куда Ractive будет рендарить этот компонент
	el: '#comments',
	//id tag <script> в котором содержится темлейт для этого компонента ( подробней в разделе темплейтов)
	template: '#comments_template',
    init       : function (options) {
		var self = this;
		//Домен куда сохранять на сервере коменты.
		var domain = options.comments_domain;
		//Текущий пользователь.
		var current_user = options.current_user;
		if (!current_user) {
			throw new Error('options.domain and options.current_user is required');
		}
		//И так для начала качнем комменты с сервера.
		Comment
			.fetch(domain)
			.then(function (comments) {

				//Разложим коменты по айдишникам что бы потом меньше бегать по списку.
				var comments_by_id = _.indexBy(comments, 'id');
				//Сортируем по дате
				comments = _.sortBy(comments, 'date');
				//Так как коменты имеет древовидную форму
				//Раскладываем ответы в массив replys отцовского комента, а так же оставляем в массиве только комменты верхненго уровня.
				comments = _.filter(comments, function (comment) {
					if (comment.reply_to && comments_by_id[comment.reply_to]) {
						var parent =...