mirror of https://github.com/writeas/writefreely
parent
ebeacff43c
commit
5e53a1788d
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,105 @@ |
||||
var postActions = function() { |
||||
var $container = He.get('moving'); |
||||
var MultiMove = function(el, id) { |
||||
var lbl = el.options[el.selectedIndex].textContent; |
||||
var collAlias = el.options[el.selectedIndex].value; |
||||
var $lbl = He.$('label[for=move-'+id+']')[0]; |
||||
$lbl.textContent = "moving to "+lbl+"..."; |
||||
var params; |
||||
if (collAlias == '|anonymous|') { |
||||
params = [id]; |
||||
} else { |
||||
params = [{ |
||||
id: id |
||||
}]; |
||||
} |
||||
var callback = function(code, resp) { |
||||
if (code == 200) { |
||||
for (var i=0; i<resp.data.length; i++) { |
||||
if (resp.data[i].code == 200) { |
||||
$lbl.innerHTML = "moved to <strong>"+lbl+"</strong>"; |
||||
var newPostURL = "/"+collAlias+"/"+resp.data[i].post.slug; |
||||
try { |
||||
// Posts page
|
||||
He.$('#post-'+resp.data[i].post.id+' > h3 > a')[0].href = newPostURL; |
||||
} catch (e) { |
||||
// Blog index
|
||||
var $article = He.get('post-'+resp.data[i].post.id); |
||||
$article.className = 'norm moved'; |
||||
if (collAlias == '|anonymous|') { |
||||
$article.innerHTML = '<p><a href="/'+resp.data[i].post.id+'">Unpublished post</a>.</p>'; |
||||
} else { |
||||
$article.innerHTML = '<p>Moved to <a style="font-weight:bold" href="'+newPostURL+'">'+lbl+'</a>.</p>'; |
||||
} |
||||
} |
||||
} else { |
||||
$lbl.innerHTML = "unable to move: "+resp.data[i].error_msg; |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
if (collAlias == '|anonymous|') { |
||||
He.postJSON("/api/posts/disperse", params, callback); |
||||
} else { |
||||
He.postJSON("/api/collections/"+collAlias+"/collect", params, callback); |
||||
} |
||||
}; |
||||
var Move = function(el, id, collAlias) { |
||||
var lbl = el.textContent; |
||||
try { |
||||
var m = lbl.match(/move to (.*)/); |
||||
lbl = m[1]; |
||||
} catch (e) { |
||||
if (collAlias == '|anonymous|') { |
||||
lbl = "draft"; |
||||
} |
||||
} |
||||
|
||||
el.textContent = "moving to "+lbl+"..."; |
||||
if (collAlias == '|anonymous|') { |
||||
params = [id]; |
||||
} else { |
||||
params = [{ |
||||
id: id |
||||
}]; |
||||
} |
||||
var callback = function(code, resp) { |
||||
if (code == 200) { |
||||
for (var i=0; i<resp.data.length; i++) { |
||||
if (resp.data[i].code == 200) { |
||||
el.innerHTML = "moved to <strong>"+lbl+"</strong>"; |
||||
el.onclick = null; |
||||
var newPostURL = "/"+collAlias+"/"+resp.data[i].post.slug; |
||||
el.href = newPostURL; |
||||
el.title = "View on "+lbl; |
||||
try { |
||||
// Posts page
|
||||
He.$('#post-'+resp.data[i].post.id+' > h3 > a')[0].href = newPostURL; |
||||
} catch (e) { |
||||
// Blog index
|
||||
var $article = He.get('post-'+resp.data[i].post.id); |
||||
$article.className = 'norm moved'; |
||||
if (collAlias == '|anonymous|') { |
||||
$article.innerHTML = '<p><a href="/'+resp.data[i].post.id+'">Unpublished post</a>.</p>'; |
||||
} else { |
||||
$article.innerHTML = '<p>Moved to <a style="font-weight:bold" href="'+newPostURL+'">'+lbl+'</a>.</p>'; |
||||
} |
||||
} |
||||
} else { |
||||
el.innerHTML = "unable to move: "+resp.data[i].error_msg; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
if (collAlias == '|anonymous|') { |
||||
He.postJSON("/api/posts/disperse", params, callback); |
||||
} else { |
||||
He.postJSON("/api/collections/"+collAlias+"/collect", params, callback); |
||||
} |
||||
}; |
||||
|
||||
return { |
||||
move: Move, |
||||
multiMove: MultiMove, |
||||
}; |
||||
}(); |
@ -0,0 +1,315 @@ |
||||
/** |
||||
* Functionality for managing local Write.as posts. |
||||
* |
||||
* Dependencies: |
||||
* h.js |
||||
*/ |
||||
function toggleTheme() { |
||||
var btns; |
||||
try { |
||||
btns = Array.prototype.slice.call(document.getElementById('belt').querySelectorAll('.tool img')); |
||||
} catch (e) {} |
||||
if (document.body.className == 'light') { |
||||
document.body.className = 'dark'; |
||||
try { |
||||
for (var i=0; i<btns.length; i++) { |
||||
btns[i].src = btns[i].src.replace('_dark@2x.png', '@2x.png'); |
||||
} |
||||
} catch (e) {} |
||||
} else if (document.body.className == 'dark') { |
||||
document.body.className = 'light'; |
||||
try { |
||||
for (var i=0; i<btns.length; i++) { |
||||
btns[i].src = btns[i].src.replace('@2x.png', '_dark@2x.png'); |
||||
} |
||||
} catch (e) {} |
||||
} else { |
||||
// Don't alter the theme
|
||||
return; |
||||
} |
||||
H.set('padTheme', document.body.className); |
||||
} |
||||
if (H.get('padTheme', 'light') != 'light') { |
||||
toggleTheme(); |
||||
} |
||||
|
||||
var deleting = false; |
||||
function delPost(e, id, owned) { |
||||
e.preventDefault(); |
||||
if (deleting) { |
||||
return; |
||||
} |
||||
|
||||
// TODO: UNDO!
|
||||
if (window.confirm('Are you sure you want to delete this post?')) { |
||||
var token; |
||||
for (var i=0; i<posts.length; i++) { |
||||
if (posts[i].id == id) { |
||||
token = posts[i].token; |
||||
break; |
||||
} |
||||
} |
||||
if (owned || token) { |
||||
// AJAX
|
||||
deletePost(id, token, function() { |
||||
// Remove post from list
|
||||
var $postEl = document.getElementById('post-' + id); |
||||
$postEl.parentNode.removeChild($postEl); |
||||
|
||||
if (posts.length == 0) { |
||||
displayNoPosts(); |
||||
return; |
||||
} |
||||
|
||||
// Fill in full page of posts
|
||||
var $postsChildren = $posts.el.getElementsByClassName('post'); |
||||
if ($postsChildren.length < postsPerPage && $postsChildren.length < posts.length) { |
||||
var lastVisiblePostID = $postsChildren[$postsChildren.length-1].id; |
||||
lastVisiblePostID = lastVisiblePostID.substr(lastVisiblePostID.indexOf('-')+1); |
||||
|
||||
for (var i=0; i<posts.length-1; i++) { |
||||
if (posts[i].id == lastVisiblePostID) { |
||||
var $moreBtn = document.getElementById('more-posts'); |
||||
if ($moreBtn) { |
||||
// Should always land here (?)
|
||||
$posts.el.insertBefore(createPostEl(posts[i-1]), $moreBtn); |
||||
} else { |
||||
$posts.el.appendChild(createPostEl(posts[i-1])); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
} else { |
||||
alert('Something went seriously wrong. Try refreshing.'); |
||||
} |
||||
} |
||||
} |
||||
var getFormattedDate = function(d) { |
||||
var mos = [ |
||||
"January", "February", "March", |
||||
"April", "May", "June", "July", |
||||
"August", "September", "October", |
||||
"November", "December" |
||||
]; |
||||
|
||||
var day = d.getDate(); |
||||
var mo = d.getMonth(); |
||||
var yr = d.getFullYear(); |
||||
return mos[mo] + ' ' + day + ', ' + yr; |
||||
}; |
||||
var posts = JSON.parse(H.get('posts', '[]')); |
||||
|
||||
var initialListPop = function() { |
||||
pages = Math.ceil(posts.length / postsPerPage); |
||||
|
||||
loadPage(page, true); |
||||
}; |
||||
|
||||
var $posts = H.getEl("posts"); |
||||
if ($posts.el == null) { |
||||
$posts = H.getEl("unsynced-posts"); |
||||
} |
||||
$posts.el.innerHTML = '<p class="status">Reading...</p>'; |
||||
var createMorePostsEl = function() { |
||||
var $more = document.createElement('div'); |
||||
var nextPage = page+1; |
||||
$more.id = 'more-posts'; |
||||
$more.innerHTML = '<p><a href="#' + nextPage + '">More...</a></p>'; |
||||
|
||||
return $more; |
||||
}; |
||||
|
||||
var localPosts = function() { |
||||
var $delPost, lastDelPost, lastInfoHTML; |
||||
var $info = He.get('unsynced-posts-info'); |
||||
|
||||
var findPostIdx = function(id) { |
||||
for (var i=0; i<posts.length; i++) { |
||||
if (posts[i].id == id) { |
||||
return i; |
||||
} |
||||
} |
||||
return -1; |
||||
}; |
||||
|
||||
var DismissError = function(e, el) { |
||||
e.preventDefault(); |
||||
var $errorMsg = el.parentNode.previousElementSibling; |
||||
$errorMsg.parentNode.removeChild($errorMsg); |
||||
var $errorMsgNav = el.parentNode; |
||||
$errorMsgNav.parentNode.removeChild($errorMsgNav); |
||||
}; |
||||
var DeletePostLocal = function(e, el, id) { |
||||
e.preventDefault(); |
||||
if (!window.confirm('Are you sure you want to delete this post?')) { |
||||
return; |
||||
} |
||||
var i = findPostIdx(id); |
||||
if (i > -1) { |
||||
lastDelPost = posts.splice(i, 1)[0]; |
||||
$delPost = H.getEl('post-'+id); |
||||
$delPost.setClass('del-undo'); |
||||
var $unsyncPosts = document.getElementById('unsynced-posts'); |
||||
var visible = $unsyncPosts.children.length; |
||||
for (var i=0; i < $unsyncPosts.children.length; i++) { // NOTE: *.children support in IE9+
|
||||
if ($unsyncPosts.children[i].className.indexOf('del-undo') !== -1) { |
||||
visible--; |
||||
} |
||||
} |
||||
if (visible == 0) { |
||||
H.getEl('unsynced-posts-header').hide(); |
||||
// TODO: fix undo functionality and don't do the following:
|
||||
H.getEl('unsynced-posts-info').hide(); |
||||
} |
||||
H.set('posts', JSON.stringify(posts)); |
||||
// TODO: fix undo functionality and re-add
|
||||
//lastInfoHTML = $info.innerHTML;
|
||||
//$info.innerHTML = 'Unsynced entry deleted. <a href="#" onclick="localPosts.undoDelete()">Undo</a>.';
|
||||
} |
||||
}; |
||||
var UndoDelete = function() { |
||||
// TODO: fix this header reappearing
|
||||
H.getEl('unsynced-posts-header').show(); |
||||
$delPost.removeClass('del-undo'); |
||||
$info.innerHTML = lastInfoHTML; |
||||
}; |
||||
|
||||
return { |
||||
dismissError: DismissError, |
||||
deletePost: DeletePostLocal, |
||||
undoDelete: UndoDelete, |
||||
}; |
||||
}(); |
||||
var createPostEl = function(post) { |
||||
var $post = document.createElement('div'); |
||||
var title = (post.title || post.id); |
||||
title = title.replace(/</g, "<"); |
||||
$post.id = 'post-' + post.id; |
||||
$post.className = 'post'; |
||||
$post.innerHTML = '<h3><a href="/' + post.id + '">' + title + '</a></h3>'; |
||||
|
||||
var posted = ""; |
||||
if (post.created) { |
||||
posted = getFormattedDate(new Date(post.created)) |
||||
} |
||||
var hasDraft = H.exists('draft' + post.id); |
||||
$post.innerHTML += '<h4><date>' + posted + '</date> <a class="action" href="/pad/' + post.id + '">edit' + (hasDraft ? 'ed' : '') + '</a> <a class="delete action" href="/' + post.id + '" onclick="delPost(event, \'' + post.id + '\')">delete</a></h4>'; |
||||
|
||||
if (post.error) { |
||||
$post.innerHTML += '<p class="error"><strong>Sync error:</strong> ' + post.error + ' <nav><a href="#" onclick="localPosts.dismissError(event, this)">dismiss</a> <a href="#" onclick="localPosts.deletePost(event, this, \''+post.id+'\')">remove post</a></nav></p>'; |
||||
} |
||||
if (post.summary) { |
||||
$post.innerHTML += '<p>' + post.summary.replace(/</g, "<") + '</p>'; |
||||
} |
||||
return $post; |
||||
}; |
||||
var loadPage = function(p, loadAll) { |
||||
if (loadAll) { |
||||
$posts.el.innerHTML = ''; |
||||
} |
||||
|
||||
var startPost = posts.length - 1 - (loadAll ? 0 : ((p-1)*postsPerPage)); |
||||
var endPost = posts.length - 1 - (p*postsPerPage); |
||||
for (var i=startPost; i>=0 && i>endPost; i--) { |
||||
$posts.el.appendChild(createPostEl(posts[i])); |
||||
} |
||||
|
||||
if (loadAll) {
|
||||
if (p < pages) { |
||||
$posts.el.appendChild(createMorePostsEl()); |
||||
} |
||||
} else { |
||||
var $moreEl = document.getElementById('more-posts'); |
||||
$moreEl.parentNode.removeChild($moreEl); |
||||
} |
||||
try { |
||||
postsLoaded(posts.length); |
||||
} catch (e) {} |
||||
}; |
||||
var getPageNum = function(url) { |
||||
var hash; |
||||
if (url) { |
||||
hash = url.substr(url.indexOf('#')+1); |
||||
} else { |
||||
hash = window.location.hash.substr(1); |
||||
} |
||||
|
||||
var page = hash || 1; |
||||
page = parseInt(page); |
||||
if (isNaN(page)) { |
||||
page = 1; |
||||
} |
||||
|
||||
return page; |
||||
}; |
||||
|
||||
var postsPerPage = 10; |
||||
var pages = 0; |
||||
var page = getPageNum(); |
||||
|
||||
window.addEventListener('hashchange', function(e) { |
||||
var newPage = getPageNum(); |
||||
var didPageIncrement = newPage == getPageNum(e.oldURL) + 1; |
||||
|
||||
loadPage(newPage, !didPageIncrement); |
||||
}); |
||||
|
||||
var deletePost = function(postID, token, callback) { |
||||
deleting = true; |
||||
|
||||
var $delBtn = document.getElementById('post-' + postID).getElementsByClassName('delete action')[0]; |
||||
$delBtn.innerHTML = '...'; |
||||
|
||||
var http = new XMLHttpRequest(); |
||||
var url = "/api/posts/" + postID + (typeof token !== 'undefined' ? "?token=" + encodeURIComponent(token) : ''); |
||||
http.open("DELETE", url, true); |
||||
http.onreadystatechange = function() { |
||||
if (http.readyState == 4) { |
||||
deleting = false; |
||||
if (http.status == 204 || http.status == 404) { |
||||
for (var i=0; i<posts.length; i++) { |
||||
if (posts[i].id == postID) { |
||||
// TODO: use this return value, along will full content, for restoring post
|
||||
posts.splice(i, 1); |
||||
break; |
||||
} |
||||
} |
||||
H.set('posts', JSON.stringify(posts)); |
||||
|
||||
callback(); |
||||
} else if (http.status == 409) { |
||||
$delBtn.innerHTML = 'delete'; |
||||
alert("Post is synced to another account. Delete the post from that account instead."); |
||||
// TODO: show "remove" button instead of "delete" now
|
||||
// Persist that state.
|
||||
// Have it remove the post locally only.
|
||||
} else { |
||||
$delBtn.innerHTML = 'delete'; |
||||
alert("Failed to delete. Please try again."); |
||||
} |
||||
} |
||||
} |
||||
http.send(); |
||||
}; |
||||
|
||||
var hasWritten = H.get('lastDoc', '') !== ''; |
||||
|
||||
var displayNoPosts = function() { |
||||
if (auth) { |
||||
$posts.el.innerHTML = ''; |
||||
return; |
||||
} |
||||
var cta = '<a href="/pad">Create a post</a> and it\'ll appear here.'; |
||||
if (hasWritten) { |
||||
cta = '<a href="/pad">Finish your post</a> and it\'ll appear here.'; |
||||
} |
||||
H.getEl("posts").el.innerHTML = '<p class="status">No posts created yet.</p><p class="status">' + cta + '</p>'; |
||||
}; |
||||
|
||||
if (posts.length == 0) { |
||||
displayNoPosts(); |
||||
} else { |
||||
initialListPop(); |
||||
} |
||||
|
@ -0,0 +1,9 @@ |
||||
ace.define("ace/theme/chrome",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-chrome",t.cssText='.ace-chrome .ace_gutter {background: #ebebeb;color: #333;overflow : hidden;}.ace-chrome .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-chrome {background-color: #FFFFFF;color: black;}.ace-chrome .ace_cursor {color: black;}.ace-chrome .ace_invisible {color: rgb(191, 191, 191);}.ace-chrome .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-chrome .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-chrome .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-chrome .ace_invalid {background-color: rgb(153, 0, 0);color: white;}.ace-chrome .ace_fold {}.ace-chrome .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-chrome .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-chrome .ace_support.ace_type,.ace-chrome .ace_support.ace_class.ace-chrome .ace_support.ace_other {color: rgb(109, 121, 222);}.ace-chrome .ace_variable.ace_parameter {font-style:italic;color:#FD971F;}.ace-chrome .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-chrome .ace_comment {color: #236e24;}.ace-chrome .ace_comment.ace_doc {color: #236e24;}.ace-chrome .ace_comment.ace_doc.ace_tag {color: #236e24;}.ace-chrome .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-chrome .ace_variable {color: rgb(49, 132, 149);}.ace-chrome .ace_xml-pe {color: rgb(104, 104, 91);}.ace-chrome .ace_entity.ace_name.ace_function {color: #0000A2;}.ace-chrome .ace_heading {color: rgb(12, 7, 255);}.ace-chrome .ace_list {color:rgb(185, 6, 144);}.ace-chrome .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-chrome .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-chrome .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-chrome .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-chrome .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-chrome .ace_gutter-active-line {background-color : #dcdcdc;}.ace-chrome .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-chrome .ace_storage,.ace-chrome .ace_keyword,.ace-chrome .ace_meta.ace_tag {color: rgb(147, 15, 128);}.ace-chrome .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-chrome .ace_string {color: #1A1AA6;}.ace-chrome .ace_entity.ace_other.ace_attribute-name {color: #994409;}.ace-chrome .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}';var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}); |
||||
(function() { |
||||
ace.require(["ace/theme/chrome"], function(m) { |
||||
if (typeof module == "object" && typeof exports == "object" && module) { |
||||
module.exports = m; |
||||
} |
||||
}); |
||||
})(); |
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,145 @@ |
||||
{{define "articles"}} |
||||
{{template "header" .}} |
||||
|
||||
<div class="snug content-container"> |
||||
|
||||
{{if .Flashes}}<ul class="errors"> |
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} |
||||
</ul>{{end}} |
||||
|
||||
<h2 id="posts-header">drafts</h2> |
||||
|
||||
{{ if .AnonymousPosts }}<div class="atoms posts"> |
||||
{{ range $el := .AnonymousPosts }}<div id="post-{{.ID}}" class="post"> |
||||
<h3><a href="/{{.ID}}" itemprop="url">{{.DisplayTitle}}</a></h3> |
||||
<h4> |
||||
<date datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</date> |
||||
<a class="action" href="/{{.ID}}/edit">edit</a> |
||||
<a class="delete action" href="/{{.ID}}" onclick="delPost(event, '{{.ID}}', true)">delete</a> |
||||
{{ if $.Collections }} |
||||
{{if gt (len $.Collections) 1}}<div class="action flat-select"> |
||||
<select id="move-{{.ID}}" onchange="postActions.multiMove(this, '{{.ID}}')" title="Move this post to one of your blogs"> |
||||
<option style="display:none"></option> |
||||
{{range $.Collections}}<option value="{{.Alias}}">{{.DisplayTitle}}</option>{{end}} |
||||
</select> |
||||
<label for="move-{{.ID}}">move to...</label> |
||||
<img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /> |
||||
</div>{{else}} |
||||
{{range $.Collections}} |
||||
<a class="action" href="/{{$el.ID}}" title="Publish this post to your blog '{{.DisplayTitle}}'" onclick="postActions.move(this, '{{$el.ID}}', '{{.Alias}}');return false">move to {{.DisplayTitle}}</a> |
||||
{{end}} |
||||
{{end}} |
||||
{{ end }} |
||||
</h4> |
||||
{{if .Summary}}<p>{{.Summary}}</p>{{end}} |
||||
</div>{{end}} |
||||
</div>{{ else }}<div id="no-posts-published"><p>You haven't saved any drafts yet.</p> |
||||
<p>They'll show up here once you do. Find your blog posts from the <a href="/me/c/">Blogs</a> page.</p> |
||||
<p class="text-cta"><a href="/">Start writing</a></p></div>{{ end }} |
||||
|
||||
<div id="moving"></div> |
||||
|
||||
<h2 id="unsynced-posts-header" style="display: none">unsynced posts</h2> |
||||
<div id="unsynced-posts-info" style="margin-top: 1em"></div> |
||||
<div id="unsynced-posts" class="atoms"></div> |
||||
|
||||
</div> |
||||
|
||||
<script src="/js/h.js"></script> |
||||
<script src="/js/postactions.js"></script> |
||||
<script> |
||||
var auth = true; |
||||
function postsLoaded(n) { |
||||
if (n == 0) { |
||||
return; |
||||
} |
||||
document.getElementById('unsynced-posts-header').style.display = 'block'; |
||||
var syncing = false; |
||||
var $pInfo = document.getElementById('unsynced-posts-info'); |
||||
$pInfo.className = 'alert info'; |
||||
var plural = n != 1; |
||||
$pInfo.innerHTML = '<p>You have <strong>'+n+'</strong> post'+(plural?'s that aren\'t':' that isn\'t')+' synced to your account yet. <a href="#" id="btn-sync">Sync '+(plural?'them':'it')+' now</a>.</p>'; |
||||
|
||||
var $noPosts = document.getElementById('no-posts-published'); |
||||
if ($noPosts != null) { |
||||
$noPosts.style.display = 'none'; |
||||
document.getElementById('posts-header').style.display = 'none'; |
||||
} |
||||
|
||||
H.getEl('btn-sync').on('click', function(e) { |
||||
e.preventDefault(); |
||||
if (syncing) { |
||||
return; |
||||
} |
||||
var http = new XMLHttpRequest(); |
||||
var params = []; |
||||
var posts = JSON.parse(H.get('posts', '[]')); |
||||
if (posts.length > 0) { |
||||
for (var i=0; i<posts.length; i++) { |
||||
params.push({id: posts[i].id, token: posts[i].token}); |
||||
} |
||||
} |
||||
|
||||
this.style.fontWeight = 'bold'; |
||||
this.innerText = 'Syncing '+(plural?'them':'it')+' now...'; |
||||
|
||||
http.open("POST", "/api/posts/claim", true); |
||||
|
||||
// Send the proper header information along with the request |
||||
http.setRequestHeader("Content-type", "application/json"); |
||||
|
||||
http.onreadystatechange = function() { |
||||
if (http.readyState == 4) { |
||||
syncing = false; |
||||
this.innerText = 'Importing '+(plural?'them':'it')+' now...'; |
||||
|
||||
if (http.status == 200) { |
||||
var res = JSON.parse(http.responseText); |
||||
if (res.data.length > 0) { |
||||
if (res.data.length != posts.length) { |
||||
// TODO: handle something that royally fucked up |
||||
console.error("Request and result array length didn't match!"); |
||||
return; |
||||
} |
||||
for (var i=0; i<res.data.length; i++) { |
||||
if (res.data[i].code == 200) { |
||||
// Post successfully claimed. |
||||
for (var j=0; j<posts.length; j++) { |
||||
// Find post in local store |
||||
if (posts[j].id == res.data[i].post.id) { |
||||
// Remove this post |
||||
posts.splice(j, 1); |
||||
break; |
||||
} |
||||
} |
||||
} else { |
||||
for (var j=0; j<posts.length; j++) { |
||||
// Find post in local store |
||||
if (posts[j].id == res.data[i].id) { |
||||
// Note the error in the local post |
||||
posts[j].error = res.data[i].error_msg; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
H.set('posts', JSON.stringify(posts)); |
||||
location.reload(); |
||||
} |
||||
} else { |
||||
// TODO: handle error visually (option to retry) |
||||
console.error("Didn't work at all, man."); |
||||
this.style.fontWeight = 'normal'; |
||||
this.innerText = 'Sync '+(plural?'them':'it')+' now'; |
||||
} |
||||
} |
||||
} |
||||
http.send(JSON.stringify(params)); |
||||
syncing = true; |
||||
}); |
||||
} |
||||
</script> |
||||
<script src="/js/posts.js"></script> |
||||
|
||||
{{template "footer" .}} |
||||
{{end}} |
@ -0,0 +1,234 @@ |
||||
{{define "upgrade"}} |
||||
<p><a href="/me/plan?to=/me/c/{{.Alias}}">Upgrade</a> for <span>$40 / year</span> to edit.</p> |
||||
{{end}} |
||||
|
||||
{{define "collection"}} |
||||
{{template "header" .}} |
||||
|
||||
<div class="content-container snug"> |
||||
<div id="overlay"></div> |
||||
|
||||
<h2>Customize {{.DisplayTitle}} <a href="/{{.Alias}}/">view blog</a></h2> |
||||
|
||||
{{if .Flashes}}<ul class="errors"> |
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} |
||||
</ul>{{end}} |
||||
|
||||
<form name="customize-form" action="/api/collections/{{.Alias}}" method="post" onsubmit="return disableSubmit()"> |
||||
<div id="collection-options"> |
||||
<div style="text-align:center"> |
||||
<h1><input type="text" name="title" id="title" value="{{.DisplayTitle}}" placeholder="Title" /></h1> |
||||
<p><input type="text" name="description" id="description" value="{{.Description}}" placeholder="Description" /></p> |
||||
</div> |
||||
|
||||
<div class="option"> |
||||
<h2><a name="preferred-url"></a>URL</h2> |
||||
<div class="section"> |
||||
{{if eq .Alias .Username}}<p style="font-size: 0.8em">This blog uses your username in its URL{{if .Federation}} and fediverse handle{{end}}. You can change it in your <a href="/me/settings">Account Settings</a>.</p>{{end}} |
||||
<ul style="list-style:none"> |
||||
<li> |
||||
{{.FriendlyHost}}/<strong>{{.Alias}}</strong>/ |
||||
</li> |
||||
<li> |
||||
<strong id="normal-handle-env" class="fedi-handle" {{if or (not .Federation) .SingleUser}}style="display:none"{{end}}>@<span id="fedi-handle">{{.Alias}}</span>@<span id="fedi-domain">{{.FriendlyHost}}</span></strong> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="option"> |
||||
<h2>Publicity</h2> |
||||
<div class="section"> |
||||
<ul style="list-style:none"> |
||||
<li> |
||||
<label><input type="radio" name="visibility" id="visibility-unlisted" value="0" {{if .IsUnlisted}}checked="checked"{{end}} /> |
||||
Unlisted |
||||
</label> |
||||
<p>This blog is visible to anyone with its link.</p> |
||||
</li> |
||||
<li> |
||||
<label class="option-text"><input type="radio" name="visibility" id="visibility-private" value="2" {{if .IsPrivate}}checked="checked"{{end}} /> |
||||
Private |
||||
</label> |
||||
<p>Only you may read this blog (while you're logged in).</p> |
||||
</li> |
||||
<li> |
||||
<label class="option-text"><input type="radio" name="visibility" id="visibility-protected" value="4" {{if .IsProtected}}checked="checked"{{end}} /> |
||||
Password-protected: <input type="password" class="low-profile" name="password" id="collection-pass" autocomplete="new-password" placeholder="{{if .IsProtected}}xxxxxxxxxxxxxxxx{{else}}a memorable password{{end}}" /> |
||||
</label> |
||||
<p>A password is required to read this blog.</p> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="option"> |
||||
<h2>Display Format</h2> |
||||
<div class="section"> |
||||
<p class="explain">Customize how your posts display on your page. |
||||
</p> |
||||
<ul style="list-style:none"> |
||||
<li> |
||||
<label><input type="radio" name="format" id="format-blog" value="blog" {{if or (not .Format) (eq .Format "blog")}}checked="checked"{{end}} /> |
||||
Blog |
||||
</label> |
||||
<p>Dates are shown. Latest posts listed first.</p> |
||||
</li> |
||||
<li> |
||||
<label class="option-text"><input type="radio" name="format" id="format-novel" value="novel" {{if eq .Format "novel"}}checked="checked"{{end}} /> |
||||
Novel |
||||
</label> |
||||
<p>No dates shown. Oldest posts first.</p> |
||||
</li> |
||||
<li> |
||||
<label class="option-text"><input type="radio" name="format" id="format-notebook" value="notebook" {{if eq .Format "notebook"}}checked="checked"{{end}} /> |
||||
Notebook |
||||
</label> |
||||
<p>No dates shown. Latest posts first.</p> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="option"> |
||||
<h2>Text Rendering</h2> |
||||
<div class="section"> |
||||
<p class="explain">Customize how plain text renders on your blog.</p> |
||||
<ul style="list-style:none"> |
||||
<li> |
||||
<label class="option-text disabled"><input type="checkbox" name="markdown" checked="checked" disabled /> |
||||
Markdown |
||||
</label> |
||||
</li> |
||||
<li> |
||||
<label><input type="checkbox" name="mathjax" {{if .RenderMathJax}}checked="checked"{{end}} /> |
||||
MathJax |
||||
</label> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="option"> |
||||
<h2>Custom CSS</h2> |
||||
<div class="section"> |
||||
<textarea id="css-editor" class="section codable" name="style_sheet">{{.StyleSheet}}</textarea> |
||||
<p class="explain">See our guide on <a href="https://guides.write.as/customizing/#custom-css">customization</a>.</p> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="option" style="text-align: center; margin-top: 4em;"> |
||||
<input type="submit" id="save-changes" value="Save changes" /> |
||||
<p><a href="/{{.Alias}}/">View Blog</a></p> |
||||
{{if ne .Alias .Username}}<p><a class="danger" href="#modal-delete" onclick="promptDelete();">Delete Blog...</a></p>{{end}} |
||||
</div> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
|
||||
<div id="modal-delete" class="modal"> |
||||
<h2>Are you sure you want to delete this blog?</h2> |
||||
<div class="body short"> |
||||
<p style="text-align:left">This will permanently erase <strong>{{.DisplayTitle}}</strong> ({{.FriendlyHost}}/{{.Alias}}) from the internet. Any posts on this blog will be saved and made into drafts (found on your <a href="/me/posts/">Drafts</a> page).</p> |
||||
<p>If you're sure you want to delete this blog, enter its name in the box below and press <strong>Delete</strong>.</p> |
||||
|
||||
<ul id="delete-errors" class="errors"></ul> |
||||
|
||||
<input id="confirm-text" placeholder="{{.Alias}}" type="text" class="boxy" style="margin-top: 0.5em;" /> |
||||
<div style="text-align:right; margin-top: 1em;"> |
||||
<a id="cancel-delete" style="margin-right:2em" href="#">Cancel</a> |
||||
<button id="btn-delete" class="danger" onclick="deleteBlog(); return false;">Delete</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<script src="/js/h.js"></script> |
||||
<script src="/js/ace.js" type="text/javascript" charset="utf-8"></script> |
||||
<script> |
||||
// Begin shared modal code |
||||
function showModal(id) { |
||||
document.getElementById('overlay').style.display = 'block'; |
||||
document.getElementById('modal-'+id).style.display = 'block'; |
||||
} |
||||
|
||||
var closeModals = function(e) { |
||||
e.preventDefault(); |
||||
document.getElementById('overlay').style.display = 'none'; |
||||
var modals = document.querySelectorAll('.modal'); |
||||
for (var i=0; i<modals.length; i++) { |
||||
modals[i].style.display = 'none'; |
||||
} |
||||
}; |
||||
H.getEl('overlay').on('click', closeModals); |
||||
H.getEl('cancel-delete').on('click', closeModals); |
||||
// end |
||||
var deleteBlog = function(e) { |
||||
if (document.getElementById('confirm-text').value != '{{.Alias}}') { |
||||
document.getElementById('delete-errors').innerHTML = '<li class="urgent">Enter <strong>{{.Alias}}</strong> in the box below.</li>'; |
||||
return; |
||||
} |
||||
// Clear errors |
||||
document.getElementById('delete-errors').innerHTML = ''; |
||||
document.getElementById('btn-delete').innerHTML = 'Deleting...'; |
||||
|
||||
var http = new XMLHttpRequest(); |
||||
var url = "/api/collections/{{.Alias}}?web=1"; |
||||
http.open("DELETE", url, true); |
||||
http.setRequestHeader("Content-type", "application/json"); |
||||
http.onreadystatechange = function() { |
||||
if (http.readyState == 4) { |
||||
if (http.status == 204) { |
||||
window.location = '/me/c/'; |
||||
} else { |
||||
var data = JSON.parse(http.responseText); |
||||
document.getElementById('delete-errors').innerHTML = '<li class="urgent">'+data.error_msg+'</li>'; |
||||
document.getElementById('btn-delete').innerHTML = 'Delete'; |
||||
} |
||||
} |
||||
}; |
||||
http.send(null); |
||||
}; |
||||
|
||||
function createHidden(theForm, key, value) { |
||||
var input = document.createElement('input'); |
||||
input.type = 'hidden'; |
||||
input.name = key; |
||||
input.value = value; |
||||
theForm.appendChild(input); |
||||
} |
||||
function disableSubmit() { |
||||
var $form = document.forms['customize-form']; |
||||
createHidden($form, 'style_sheet', cssEditor.getSession().getValue()); |
||||
createHidden($form, 'script', jsEditor.getSession().getValue()); |
||||
var $btn = document.getElementById("save-changes"); |
||||
$btn.value = "Saving changes..."; |
||||
$btn.disabled = true; |
||||
return true; |
||||
} |
||||
function promptDelete() { |
||||
showModal("delete"); |
||||
} |
||||
|
||||
var $fediDomain = document.getElementById('fedi-domain'); |
||||
var $fediCustomDomain = document.getElementById('fedi-custom-domain'); |
||||
var $customDomain = document.getElementById('domain-alias'); |
||||
var $customHandleEnv = document.getElementById('custom-handle-env'); |
||||
var $normalHandleEnv = document.getElementById('normal-handle-env'); |
||||
|
||||
var opt = { |
||||
showLineNumbers: false, |
||||
showPrintMargin: 0, |
||||
}; |
||||
var theme = "ace/theme/chrome"; |
||||
var cssEditor = ace.edit("css-editor"); |
||||
cssEditor.setTheme(theme); |
||||
cssEditor.session.setMode("ace/mode/css"); |
||||
cssEditor.setOptions(opt); |
||||
var jsEditor = ace.edit("js-editor"); |
||||
jsEditor.setTheme(theme); |
||||
jsEditor.session.setMode("ace/mode/javascript"); |
||||
jsEditor.setOptions(opt); |
||||
</script> |
||||
|
||||
{{template "footer" .}} |
||||
{{end}} |
@ -0,0 +1,111 @@ |
||||
{{define "collections"}} |
||||
{{template "header" .}} |
||||
|
||||
<div class="snug content-container"> |
||||
|
||||
{{if .Flashes}}<ul class="errors"> |
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} |
||||
</ul>{{end}} |
||||
|
||||
<h2>blogs</h2> |
||||
<ul class="atoms collections"> |
||||
{{range $i, $el := .Collections}}<li class="collection"><h3> |
||||
<a class="title" href="/{{.Alias}}/">{{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}</a> |
||||
</h3> |
||||
<h4> |
||||
<a class="action new-post" href="/#{{.Alias}}">new post</a> |
||||
<a class="action" href="/me/c/{{.Alias}}">customize</a> |
||||
<a class="action" href="/me/c/{{.Alias}}/stats">stats</a> |
||||
</h4> |
||||
{{if .Description}}<p class="description">{{.Description}}</p>{{end}} |
||||
</li>{{end}} |
||||
<li id="create-collection"> |
||||
{{if not .NewBlogsDisabled}} |
||||
<form method="POST" action="/api/collections" id="new-collection-form" onsubmit="return createCollection()"> |
||||
<h4> |
||||
<input type="text" name="title" placeholder="Blog name" id="blog-name"> |
||||
<input type="hidden" name="web" value="true" /> |
||||
<input type="submit" value="Create" id="create-collection-btn"> |
||||
</h4> |
||||
</form> |
||||
{{end}} |
||||
</li> |
||||
</ul> |
||||
{{if not .NewBlogsDisabled}}<p style="margin-top:0"><a id="new-collection" href="#new-collection">New blog</a></p>{{end}} |
||||
|
||||
</div> |
||||
|
||||
{{template "foot" .}} |
||||
|
||||
<script src="/js/h.js"></script> |
||||
<script> |
||||
function createCollection() { |
||||
var input = He.get('blog-name'); |
||||
if (input.value == "") { |
||||
return false; |
||||
} |
||||
var form = He.get('new-collection-form'); |
||||
var submit = He.get('create-collection-btn'); |
||||
submit.value = "Creating..."; |
||||
submit.disabled = "disabled"; |
||||
He.postJSON("/api/collections", { |
||||
title: input.value, |
||||
web: true |
||||
}, function(code, data) { |
||||
if (data.code == 201) { |
||||
location.reload(); |
||||
} else { |
||||
var $createColl = document.getElementById('create-collection'); |
||||
var $submit = $createColl.querySelector('input[type=submit]'); |
||||
$submit.value = "Create"; |
||||
$submit.disabled = ""; |
||||
var $err = $createColl.querySelector('.error'); |
||||
if (data.code == 409) { |
||||
if ($err === null) { |
||||
var url = He.create('span'); |
||||
url.className = "error"; |
||||
url.innerText = "This name is taken."; |
||||
$createColl.appendChild(url); |
||||
} else { |
||||
$err.innerText = "This name is taken."; |
||||
} |
||||
} else { |
||||
if ($err === null) { |
||||
var url = He.create('span'); |
||||
url.className = "error"; |
||||
url.innerText = data.error_msg; |
||||
$createColl.appendChild(url); |
||||
} else { |
||||
$err.innerText = "This name is taken."; |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
return false; |
||||
}; |
||||
(function() { |
||||
H.getEl('new-collection').on('click', function(e) { |
||||
e.preventDefault(); |
||||
var collForm = He.get('create-collection'); |
||||
if (collForm.style.display == '' || collForm.style.display == 'none') { |
||||
// Show form |
||||
this.textContent = "Cancel"; |
||||
collForm.style.display = 'list-item'; |
||||
collForm.querySelector('input[type=text]').focus(); |
||||
return; |
||||
} |
||||
// Hide form |
||||
this.textContent = "New blog"; |
||||
collForm.style.display = 'none'; |
||||
}); |
||||
if (location.hash == '#new-collection' || location.hash == '#new') { |
||||
var collForm = He.get('create-collection'); |
||||
collForm.style.display = 'list-item'; |
||||
collForm.querySelector('input[type=text]').focus(); |
||||
He.get('new-collection').textContent = "Cancel"; |
||||
} |
||||
}()); |
||||
</script> |
||||
|
||||
{{template "body-end" .}} |
||||
{{end}} |
@ -0,0 +1,28 @@ |
||||
{{define "export"}} |
||||
{{template "header" .}} |
||||
|
||||
<div class="snug content-container"> |
||||
<h2 id="posts-header">Export</h2> |
||||
<p>Your data on {{.SiteName}} is always free. Download and back-up your work any time.</p> |
||||
|
||||
<table class="classy export"> |
||||
<tr> |
||||
<th style="width: 40%">Export</th> |
||||
<th colspan="2">Format</th> |
||||
</tr> |
||||
<tr> |
||||
<th>Posts</th> |
||||
<td><p class="text-cta"><a href="/me/posts/export.csv">CSV</a></p></td> |
||||
<td><p class="text-cta"><a href="/me/posts/export.zip">ZIP</a></p></td> |
||||
</tr> |
||||
<tr> |
||||
<th>User + Blogs + Posts</th> |
||||
<td><p class="text-cta"><a href="/me/export.json">JSON</a></p></td> |
||||
<td><p class="text-cta"><a href="/me/export.json?pretty=1">Prettified</a></p></td> |
||||
</tr> |
||||
</table> |
||||
|
||||
</div> |
||||
|
||||
{{template "footer" .}} |
||||
{{end}} |
@ -0,0 +1,35 @@ |
||||
{{define "footer"}} |
||||
{{template "foot" .}} |
||||
{{template "body-end" .}} |
||||
{{end}} |
||||
{{define "foot"}} |
||||
</div> |
||||
<footer> |
||||
<hr /> |
||||
<nav> |
||||
<a class="home" href="/">{{.SiteName}}</a> |
||||
<a href="/about">about</a> |
||||
<a href="https://writefreely.org/guide" target="guides">guide</a> |
||||
<a href="/privacy">privacy</a> |
||||
<a>{{.Version}}</a> |
||||
</nav> |
||||
</footer> |
||||
|
||||
<script type="text/javascript"> |
||||
try { // Google Fonts |
||||
WebFontConfig = { |
||||
custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] } |
||||
}; |
||||
(function() { |
||||
var wf = document.createElement('script'); |
||||
wf.src = '/js/webfont.js'; |
||||
wf.type = 'text/javascript'; |
||||
wf.async = 'true'; |
||||
var s = document.getElementsByTagName('script')[0]; |
||||
s.parentNode.insertBefore(wf, s); |
||||
})(); |
||||
} catch (e) { /* ¯\_(ツ)_/¯ */ } |
||||
</script> |
||||
{{end}} |
||||
{{define "body-end"}}</body> |
||||
</html>{{end}} |
@ -0,0 +1,37 @@ |
||||
{{define "header"}}<!DOCTYPE HTML> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
|
||||
<title>{{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}—{{end}} {{.SiteName}}</title> |
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/write.css" /> |
||||
<link rel="shortcut icon" href="/favicon.ico" /> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
<meta name="theme-color" content="#888888" /> |
||||
<meta name="apple-mobile-web-app-title" content="{{.SiteName}}"> |
||||
<link rel="apple-touch-icon" sizes="152x152" href="/img/touch-icon-152.png"> |
||||
<link rel="apple-touch-icon" sizes="167x167" href="/img/touch-icon-167.png"> |
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/touch-icon-180.png"> |
||||
</head> |
||||
<body id="me"> |
||||
<header> |
||||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1> |
||||
<nav id="user-nav"> |
||||
<nav class="dropdown-nav"> |
||||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul> |
||||
<li><a href="/me/settings">Account settings</a></li> |
||||
<li><a href="/me/export">Export</a></li> |
||||
<li class="separator"><hr /></li> |
||||
<li><a href="/me/logout">Log out</a></li> |
||||
</ul></li> |
||||
</ul> |
||||
</nav> |
||||
<nav class="tabs"> |
||||
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a> |
||||
<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a> |
||||
</nav> |
||||
</nav> |
||||
</header> |
||||
<div id="official-writing"> |
||||
{{end}} |
@ -0,0 +1,83 @@ |
||||
{{define "settings"}} |
||||
{{template "header" .}} |
||||
|
||||
<style type="text/css"> |
||||
.option { margin: 2em 0em; } |
||||
h3 { font-weight: normal; } |
||||
.section > *:not(input) { font-size: 0.86em; } |
||||
</style> |
||||
<div class="content-container snug regular"> |
||||
<h2>{{if .IsLogOut}}Before you go...{{else}}Account Settings{{end}}</h2> |
||||
{{if .Flashes}}<ul class="errors"> |
||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} |
||||
</ul>{{end}} |
||||
|
||||
{{ if .IsLogOut }} |
||||
<div class="alert info"> |
||||
<p class="introduction">Please add an <strong>email address</strong> and/or <strong>passphrase</strong> so you can log in again later.</p> |
||||
</div> |
||||
{{ else }} |
||||
<div class="option"> |
||||
<p>Change your account settings here.</p> |
||||
</div> |
||||
|
||||
<form method="post" action="/api/me/self" autocomplete="false"> |
||||
<div class="option"> |
||||
<h3>Username</h3> |
||||
<div class="section"> |
||||
<input type="text" name="username" value="{{.Username}}" tabindex="1" /> |
||||
<input type="submit" value="Update" style="margin-left: 1em;" /> |
||||
</div> |
||||
</div> |
||||
</form> |
||||
{{ end }} |
||||
|
||||
<form method="post" action="/api/me/self" autocomplete="false"> |
||||
<input type="hidden" name="logout" value="{{.IsLogOut}}" /> |
||||
<div class="option"> |
||||
<h3>Passphrase</h3> |
||||
<div class="section"> |
||||
{{if and (not .HasPass) (not .IsLogOut)}}<div class="alert info"><p>Add a passphrase to easily log in to your account.</p></div>{{end}} |
||||
{{if .HasPass}}<p>Current passphrase</p> |
||||
<input type="password" name="current-pass" placeholder="Current passphrase" tabindex="1" /> <input class="show" type="checkbox" id="show-cur-pass" /><label for="show-cur-pass"> Show</label> |
||||
<p>New passphrase</p> |
||||
{{end}} |
||||
{{if .IsLogOut}}<input type="text" value="{{.Username}}" style="display:none" />{{end}} |
||||
<input type="password" name="new-pass" autocomplete="new-password" placeholder="New passphrase" tabindex="{{if .IsLogOut}}1{{else}}2{{end}}" /> <input class="show" type="checkbox" id="show-new-pass" /><label for="show-new-pass"> Show</label> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="option"> |
||||
<h3>Email</h3> |
||||
<div class="section"> |
||||
{{if and (not .Email) (not .IsLogOut)}}<div class="alert info"><p>Add your email to get:</p> |
||||
<ul> |
||||
<li>No-passphrase login</li> |
||||
<li>Account recovery if you forget your passphrase</li> |
||||
</ul></div>{{end}} |
||||
<input type="email" name="email" style="letter-spacing: 1px" placeholder="Email address" value="{{.Email}}" size="40" tabindex="{{if .IsLogOut}}2{{else}}3{{end}}" /> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="option" style="text-align: center; margin-top: 4em;"> |
||||
<input type="submit" value="Save changes" tabindex="4" /> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
|
||||
<script> |
||||
var showChecks = document.querySelectorAll('input.show'); |
||||
for (var i=0; i<showChecks.length; i++) { |
||||
showChecks[i].addEventListener('click', function() { |
||||
var prevEl = this.previousElementSibling; |
||||
if (prevEl.type == "password") { |
||||
prevEl.type = "text"; |
||||
} else { |
||||
prevEl.type = "password"; |
||||
} |
||||
}); |
||||
} |
||||
</script> |
||||
|
||||
{{template "footer" .}} |
||||
{{end}} |
@ -0,0 +1,53 @@ |
||||
{{define "stats"}} |
||||
{{template "header" .}} |
||||
<style> |
||||
table.classy th { text-align: left } |
||||
table.classy th.num { text-align: right } |
||||
td + td { |
||||
padding-left: 0.5em; |
||||
padding-right: 0.5em; |
||||
} |
||||
td.num { |
||||
text-align: right; |
||||
} |
||||
table.classy.export a { text-transform: inherit; } |
||||
td.none { |
||||
font-style: italic; |
||||
} |
||||
</style> |
||||
|
||||
<div class="content-container snug"> |
||||
<h2 id="posts-header">{{if .Collection}}{{.Collection.DisplayTitle}} {{end}}Stats</h2> |
||||
|
||||
<p>Stats for all time.</p> |
||||
|
||||
{{if .Federation}} |
||||
<h3>Fediverse stats</h3> |
||||
<table id="fediverse" class="classy export"> |
||||
<tr> |
||||
<th>Followers</th> |
||||
</tr> |
||||
<tr> |
||||
<td>{{.APFollowers}}</td> |
||||
</tr> |
||||
</table> |
||||
{{end}} |
||||
|
||||
<h3>Top {{len .TopPosts}} posts</h3> |
||||
<table class="classy export"> |
||||
<tr> |
||||
<th>Post</th> |
||||
{{if not .Collection}}<th>Blog</th>{{end}} |
||||
<th class="num">Total Views</th> |
||||
</tr> |
||||
{{range .TopPosts}}<tr> |
||||
<td style="word-break: break-all;"><a href="{{if .Collection}}{{.Collection.CanonicalURL}}{{.Slug.String}}{{else}}/{{.ID}}{{end}}">{{if ne .Title.String ""}}{{.Title.String}}{{else}}<em>{{.ID}}</em>{{end}}</a></td> |
||||
{{ if not $.Collection }}<td>{{if .Collection}}<a href="{{.Collection.CanonicalURL}}">{{.Collection.Title}}</a>{{else}}<em>Draft</em>{{end}}</td>{{ end }} |
||||
<td class="num">{{.ViewCount}}</td> |
||||
</tr>{{end}} |
||||
</table> |
||||
|
||||
</div> |
||||
|
||||
{{template "footer" .}} |
||||
{{end}} |
Loading…
Reference in new issue