Skip to content

Instantly share code, notes, and snippets.

@xbreaker
Last active August 11, 2021 08:54
Show Gist options
  • Save xbreaker/37c0fd11111e514e406038394b812bb1 to your computer and use it in GitHub Desktop.
Save xbreaker/37c0fd11111e514e406038394b812bb1 to your computer and use it in GitHub Desktop.
Staticman
.comment-thread {
box-sizing: border-box;
max-width: 100%;
margin: auto;
border: 1px solid transparent; /* Removes margin collapse */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
line-height: 1.4;
font-size: 16px;
color: rgba(0, 0, 0, 0.85);
padding-top: 30px;
}
body.dark .comment-thread {
color: rgba(255, 255, 255, 0.85);
}
.comment-thread button {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
font-size: 14px;
padding: 0px 8px;
color: rgba(0, 0, 0, 0.85);
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
body.dark .comment-thread button {
background-color: #9d9d9d;
}
.comment-thread button:hover,
.comment-thread button:focus,
.comment-thread button:active {
cursor: pointer;
background-color: #ecf0f1;
}
body.dark .comment-thread button:hover,
body.dark .comment-thread button:focus,
body.dark .comment-thread button:active {
background-color: #838383;
}
.m-0 {
margin: 0;
}
.sr-only {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
/* Comment */
.comment {
position: relative;
margin: 15px auto;
}
.comment-heading {
display: flex;
align-items: center;
height: 50px;
font-size: 14px;
}
.comment-avatar {
width: 32px;
height: 32px;
}
.comment-avatar img {
width: 32px;
height: 32px;
border-radius: 4px;
}
.comment-info {
color: rgba(0, 0, 0, 0.5);
margin-left: 10px;
}
body.dark .comment-info {
color: rgba(255, 255, 255, 0.5);
}
.comment-author {
color: rgba(0, 0, 0, 0.85);
font-weight: bold;
text-decoration: none;
}
body.dark .comment-author {
color: rgba(255, 255, 255, 0.85);
}
.comment-author:hover {
text-decoration: underline;
}
.comment-owner {
color: #47c02e;
}
.replies {
margin-left: 20px;
}
/* Adjustments for the comment border links */
.comment-border-link {
display: block;
position: absolute;
top: 50px;
left: 0;
width: 12px;
height: calc(100% - 50px);
border-left: 4px solid transparent;
border-right: 4px solid transparent;
background-color: rgba(0, 0, 0, 0.1);
background-clip: padding-box;
}
body.dark .comment-border-link {
background-color: rgba(255, 255, 255, 0.1);
}
.comment-border-link:hover {
background-color: rgba(0, 0, 0, 0.3);
}
.comment-body {
padding-left: 28px;
}
.comment-body p {
margin-block-start: 1em;
margin-block-end: 1em;
white-space: pre-line;
}
.comment-body a {
box-shadow: 0 1px;
}
.comment-body .reply-form button {
padding: 0px 8px;;
}
.replies {
margin-left: 28px;
}
/* Adjustments for toggleable comments */
details.comment summary {
position: relative;
list-style: none;
cursor: pointer;
}
details.comment summary::-webkit-details-marker {
display: none;
}
details.comment:not([open]) .comment-heading {
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}
.comment-heading::after {
display: inline-block;
position: absolute;
right: 5px;
align-self: center;
font-size: 12px;
color: rgba(0, 0, 0, 0.55);
}
details.comment[open] .comment-heading::after {
content: "-";
}
details.comment:not([open]) .comment-heading::after {
content: "+";
}
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
/* Resets cursor, and removes prompt text on Internet Explorer */
.comment-heading {
cursor: default;
}
details.comment[open] .comment-heading::after,
details.comment:not([open]) .comment-heading::after {
content: " ";
}
}
.reply-form.loading {
opacity: 0.5;
cursor: not-allowed;
}
.reply-form .notice {
padding: 10px 5px;
margin: 5px 5px 0;
display: none;
border-radius: 3px;
}
.reply-form .notice.sending {
background-color: #fafa2e;
display: block;
}
.reply-form .notice.success {
background-color: #3bfa2e;
display: block;
}
.reply-form .notice.error {
background-color: #ff9871;
display: block;
}
.reply-form input, .reply-form textarea {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 16px;
margin-top: 15px;
margin-bottom: 5px;
border: 1px solid rgb(188, 188, 188);
border-radius: 3px;
padding: 4px;
}
body.dark .reply-form input,
body.dark .reply-form textarea {
color: white;
}
.reply-form textarea {
width: 100%;
max-width: 100%;
margin-top: 15px;
margin-bottom: 5px;
padding: 4px;
}
.reply-form input {
width: 50%;
display: block;
}
.reply-form button {
margin: 15px 0 5px 0;
padding: 5px;
}
.d-none {
display: none;
}
<section id="comment-thread" class="js-comments comment-thread">
{{ if $.Site.Data.comments }}
{{ $comments := index $.Site.Data.comments .Slug }}
{{ if $comments }}
{{ $c := len $comments }}
{{ if (and (eq (mod $c 10) 1) (ne (mod $c 100) 11)) }}
<h3>{{ $c }} комментарий</h3>
{{ else }}
{{ if (and (ge (mod $c 10) 2) (le (mod $c 10) 4) (or (lt (mod $c 100) 10) (ge (mod $c 100) 20))) }}
<h3>{{ $c }} комментария</h3>
{{ else }}
<h3>{{ $c }} комментариев</h3>
{{ end }}
{{ end }}
{{ end }}
{{ $.Scratch.Set "hasComments" 0 }}
{{ range $comments }}
{{ if not .thread }}
{{ $.Scratch.Add "hasComments" 1 }}
{{ $.Scratch.Set "hasReplies" 0 }}
{{ $.Scratch.Set "thread" ._id }}
{{ $.Scratch.SetInMap "replyIndices" ._id 0 }}
<details open class="comment" id="comment-{{ $.Scratch.Get "hasComments" }}" data-thread="{{._id}}" data-id="{{._id}}" data-name="{{ .name }}">
<a href="#comment-{{ $.Scratch.Get "hasComments" }}" class="comment-border-link">
<span class="sr-only">Перейти к первому комментарию</span>
</a>
<summary>
<div class="comment-heading">
<div class="comment-avatar">
<img src="https://www.gravatar.com/avatar/{{ .email }}?s=48&d=identicon" alt="{{ .name }}'s gravatar">
</div>
<div class="comment-info">
<span class="comment-author">{{ .name }}</span>
<p class="m-0">
{{ if eq .email $.Site.Params.emailhash }}<span class="comment-owner">автор • </span>{{ end }}<time datetime="{{ .date }}">{{ dateFormat (default "2 Jan 2006 15:04:05" .Site.Params.dateformat) .date }}</time>
</p>
</div>
</div>
</summary>
<div class="comment-body">
<p>{{ .message | markdownify }}</p>
<button data-toggle="reply-form">Ответить</button>
</div>
<div class="replies">
{{ range $comments }}
{{ if eq .thread ($.Scratch.Get "thread") }}
{{ $.Scratch.Add "hasReplies" 1 }}
{{ $.Scratch.SetInMap "replyIndices" ._id ($.Scratch.Get "hasReplies") }}
<details open class="comment" id="comment-{{ $.Scratch.Get "hasComments" }}r{{ $.Scratch.Get "hasReplies" }}" data-thread="{{.thread}}" data-id="{{._id}}" data-name="{{ .name }}">
<a href="#comment-{{ $.Scratch.Get "hasComments" }}r{{ $.Scratch.Get "hasReplies" }}" class="comment-border-link">
<span class="sr-only">Перейти к первому комментарию</span>
</a>
{{- $replyTargetIndex := (index ($.Scratch.Get "replyIndices") .parent) -}}
{{- $replyLinkEnd := cond (eq $replyTargetIndex 0) "" (print "r" $replyTargetIndex) -}}
<summary>
<div class="comment-heading">
<div class="comment-avatar">
<img src="https://www.gravatar.com/avatar/{{ .email }}?s=48&d=identicon" alt="{{ .name }}'s gravatar">
</div>
<div class="comment-info">
<span class="comment-author">{{ .name }}</span> ответил <a class="comment-author" href='#comment-{{ $.Scratch.Get "hasComments" }}{{ $replyLinkEnd }}' class='comment-reply-target' title='{{ .parent }}'> {{ .parentName }}</a>
<p class="m-0">
{{ if eq .email $.Site.Params.emailhash }}<span class="comment-owner">автор</span> • {{ end }}<time datetime="{{ .date }}">{{ dateFormat (default "2 Jan 2006 15:04:05" .Site.Params.dateformat) .date }}</time>
</p>
</div>
</div>
</summary>
<div class="comment-body">
<p>{{ .message | markdownify }}</p>
<button data-toggle="reply-form">Ответить</button>
</div>
</details>
{{ end }}
{{ end }}
</div>
</details>
{{ $.Scratch.Delete "replyIndices" }}
{{ end }}
{{ end }}
{{ end }}
<form id="reply-form" class="js-form reply-form" method="post" action="{{ .Site.Params.staticman.api }}">
<h3 id="reply-form-head">Добавить комментарий</h3>
<div id="notice" class="notice"></div>
<input type="hidden" name="options[slug]" value="{{ .Slug }}">
<input type="hidden" name="options[parent]" value="{{ .Permalink }}">
<input id="input-thread" type="hidden" name="fields[thread]" value="">
<input id="input-parent" type="hidden" name="fields[parent]" value="">
<input id="input-parentName" type="hidden" name="fields[parentName]" value="">
{{ if .Site.Params.staticman.recaptcha.enabled }}
<input type="hidden" name="options[reCaptcha][siteKey]" value="{{ .Site.Params.staticman.recaptcha.sitekey }}">
<input type="hidden" name="options[reCaptcha][secret]" value="{{ .Site.Params.staticman.recaptcha.secret }}">
{{ end }}
<textarea name="fields[message]" placeholder="Вы можете использовать синтаксис Markdown" rows="6" required></textarea>
<input name="fields[name]" type="text" placeholder="имя" required/>
<input type="email" name="fields[email]" placeholder="email" required/>
{{ if .Site.Params.staticman.recaptcha.enabled }}
<div class="g-recaptcha" data-sitekey="{{ .Site.Params.staticman.recaptcha.sitekey }}"></div>
{{ end }}
<button id="submit">Отправить</button>
<button id="cancel-button" class="d-none" data-toggle="reply-form-cancel">Отменить</button>
</form>
</section>
document.addEventListener(
"click",
function(event) {
const target = event.target,
I = function(id) {
return document.getElementById(id);
},
C = function(cl) {
return document.getElementsByClassName(cl)[0];
};
if (target.matches("[data-toggle='reply-form']")) {
var cancel = I("cancel-button"),
comment = target.parentNode.parentNode,
author = comment.getAttribute("data-name");
target.parentNode.appendChild(I("reply-form"));
target.classList.toggle("d-none");
I("input-thread").value = comment.getAttribute("data-thread");
I("input-parent").value = comment.getAttribute("data-id");
I("input-parentName").value = author;
I("reply-form-head").innerHTML = "Ваш ответ " + author;
if(cancel.classList.contains("d-none"))
cancel.classList.toggle("d-none");
}
if (target.matches("[data-toggle='reply-form-cancel']")) {
I("reply-form-head").innerHTML = "Добавить комментарий";
C("d-none").classList.toggle("d-none");
I("cancel-button").classList.toggle("d-none");
I("comment-thread").appendChild(I("reply-form"));
}
},
false
);
const form = document.querySelector("#reply-form");
form.addEventListener("submit", submitEvent => {
submitEvent.preventDefault();
const notice = document.getElementById("notice");
const fd = new FormData(form);
const xhr = new XMLHttpRequest();
const json = {}
fd.forEach(function(value, prop){
json[prop] = value
})
// convert json to urlencoded query string
// SOURCE: https://stackoverflow.com/a/37562814 (comments)
const formBody = Object.keys(json).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(json[key])).join('&')
submitEvent.submitter.setAttribute("disabled", "disabled");
form.classList.toggle("loading");
notice.innerHTML = "Комментарий отправляется";
notice.classList.add("sending");
xhr.open("POST", form.action);
xhr.onreadystatechange = function (event) {
if (xhr.readyState == 4) {
if(xhr.status == 200) {
form.classList.toggle("loading");
notice.innerHTML = "Комментарий отправлен успешно";
notice.classList.remove("error");
notice.classList.remove("sending");
notice.classList.add("success");
submitEvent.submitter.removeAttribute("disabled");
form.reset();
} else {
let response = JSON.parse(xhr.responseText)
form.classList.toggle("loading");
if(response && response.errorCode === 'RECAPTCHA_INVALID_INPUT_RESPONSE') {
notice.innerHTML = "Ошибка отправки комментария (reCaptcha не пройдена)";
} else {
notice.innerHTML = "Ошибка отправки комментария";
}
notice.classList.remove("success");
notice.classList.remove("sending");
notice.classList.add("error");
submitEvent.submitter.removeAttribute("disabled");
}
}
};
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.send(formBody);
});
{{- if (and (eq .Kind "page") (ne .Layout "archives") (ne .Layout "search")) }}
{{- if not .Site.Params.assets.disableFingerprinting }}
{{- $comments := slice (resources.Get "js/comments.js") | resources.Concat "assets/js/comments.js" | minify | fingerprint }}
<script defer crossorigin="anonymous" src="{{ $comments.RelPermalink }}" integrity="{{ $comments.Data.Integrity }}"></script>
{{- else }}
{{- $comments := slice (resources.Get "js/comments.js") | resources.Concat "assets/js/comments.js" | minify }}
<script defer crossorigin="anonymous" src="{{ $comments.RelPermalink }}"></script>
{{- end }}
{{- end }}
{{- if .Site.Params.staticman.recaptcha.enabled -}}
<script src='https://www.google.com/recaptcha/api.js'></script>
{{- end -}}
comments:
allowedFields: ["name", "email", "message", "thread", "parentName", "parent"]
branch : "main"
commitMessage : "New comment from {fields.name} on {options.slug}"
path: "data/comments/{options.slug}"
filename : "comment-{@timestamp}"
format : "yaml"
moderation : true
requiredFields : ["name", "email", "message"]
pullRequestBody : "Dear human,\n\nHere's a new entry for your approval. :tada:\n\nMerge the pull request to accept it, or close it to send it away.\n\n:heart: Your friends [Staticman](https://staticman.net) && [@xbreaker](https://aybe.org) :muscle:\n\n---\n"
transforms:
email : md5
generatedFields:
date:
type : "date"
options:
format : "timestamp-seconds"
reCaptcha:
enabled : false
siteKey : ""
secret : ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment