mirror of https://github.com/writeas/writefreely
commit
38f3eec8e0
@ -0,0 +1,128 @@ |
|||||||
|
// Copyright 2014-2018 The Gogs Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style license that can be
|
||||||
|
// found in the LICENSE file of the Gogs project (github.com/gogs/gogs).
|
||||||
|
|
||||||
|
package appstats |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"math" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// Borrowed from github.com/gogs/gogs/pkg/tool
|
||||||
|
|
||||||
|
// Seconds-based time units
|
||||||
|
const ( |
||||||
|
Minute = 60 |
||||||
|
Hour = 60 * Minute |
||||||
|
Day = 24 * Hour |
||||||
|
Week = 7 * Day |
||||||
|
Month = 30 * Day |
||||||
|
Year = 12 * Month |
||||||
|
) |
||||||
|
|
||||||
|
func computeTimeDiff(diff int64) (int64, string) { |
||||||
|
diffStr := "" |
||||||
|
switch { |
||||||
|
case diff <= 0: |
||||||
|
diff = 0 |
||||||
|
diffStr = "now" |
||||||
|
case diff < 2: |
||||||
|
diff = 0 |
||||||
|
diffStr = "1 second" |
||||||
|
case diff < 1*Minute: |
||||||
|
diffStr = fmt.Sprintf("%d seconds", diff) |
||||||
|
diff = 0 |
||||||
|
|
||||||
|
case diff < 2*Minute: |
||||||
|
diff -= 1 * Minute |
||||||
|
diffStr = "1 minute" |
||||||
|
case diff < 1*Hour: |
||||||
|
diffStr = fmt.Sprintf("%d minutes", diff/Minute) |
||||||
|
diff -= diff / Minute * Minute |
||||||
|
|
||||||
|
case diff < 2*Hour: |
||||||
|
diff -= 1 * Hour |
||||||
|
diffStr = "1 hour" |
||||||
|
case diff < 1*Day: |
||||||
|
diffStr = fmt.Sprintf("%d hours", diff/Hour) |
||||||
|
diff -= diff / Hour * Hour |
||||||
|
|
||||||
|
case diff < 2*Day: |
||||||
|
diff -= 1 * Day |
||||||
|
diffStr = "1 day" |
||||||
|
case diff < 1*Week: |
||||||
|
diffStr = fmt.Sprintf("%d days", diff/Day) |
||||||
|
diff -= diff / Day * Day |
||||||
|
|
||||||
|
case diff < 2*Week: |
||||||
|
diff -= 1 * Week |
||||||
|
diffStr = "1 week" |
||||||
|
case diff < 1*Month: |
||||||
|
diffStr = fmt.Sprintf("%d weeks", diff/Week) |
||||||
|
diff -= diff / Week * Week |
||||||
|
|
||||||
|
case diff < 2*Month: |
||||||
|
diff -= 1 * Month |
||||||
|
diffStr = "1 month" |
||||||
|
case diff < 1*Year: |
||||||
|
diffStr = fmt.Sprintf("%d months", diff/Month) |
||||||
|
diff -= diff / Month * Month |
||||||
|
|
||||||
|
case diff < 2*Year: |
||||||
|
diff -= 1 * Year |
||||||
|
diffStr = "1 year" |
||||||
|
default: |
||||||
|
diffStr = fmt.Sprintf("%d years", diff/Year) |
||||||
|
diff = 0 |
||||||
|
} |
||||||
|
return diff, diffStr |
||||||
|
} |
||||||
|
|
||||||
|
// TimeSincePro calculates the time interval and generate full user-friendly string.
|
||||||
|
func TimeSincePro(then time.Time) string { |
||||||
|
now := time.Now() |
||||||
|
diff := now.Unix() - then.Unix() |
||||||
|
|
||||||
|
if then.After(now) { |
||||||
|
return "future" |
||||||
|
} |
||||||
|
|
||||||
|
var timeStr, diffStr string |
||||||
|
for { |
||||||
|
if diff == 0 { |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
diff, diffStr = computeTimeDiff(diff) |
||||||
|
timeStr += ", " + diffStr |
||||||
|
} |
||||||
|
return strings.TrimPrefix(timeStr, ", ") |
||||||
|
} |
||||||
|
|
||||||
|
func logn(n, b float64) float64 { |
||||||
|
return math.Log(n) / math.Log(b) |
||||||
|
} |
||||||
|
|
||||||
|
func humanateBytes(s uint64, base float64, sizes []string) string { |
||||||
|
if s < 10 { |
||||||
|
return fmt.Sprintf("%d B", s) |
||||||
|
} |
||||||
|
e := math.Floor(logn(float64(s), base)) |
||||||
|
suffix := sizes[int(e)] |
||||||
|
val := float64(s) / math.Pow(base, math.Floor(e)) |
||||||
|
f := "%.0f" |
||||||
|
if val < 10 { |
||||||
|
f = "%.1f" |
||||||
|
} |
||||||
|
|
||||||
|
return fmt.Sprintf(f+" %s", val, suffix) |
||||||
|
} |
||||||
|
|
||||||
|
// FileSize calculates the file size and generate user-friendly string.
|
||||||
|
func FileSize(s int64) string { |
||||||
|
sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} |
||||||
|
return humanateBytes(uint64(s), 1024, sizes) |
||||||
|
} |
@ -0,0 +1,235 @@ |
|||||||
|
{{define "pad"}}<!DOCTYPE HTML> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
|
||||||
|
<title>{{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}}</title> |
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/write.css" /> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||||
|
|
||||||
|
<meta name="google" value="notranslate"> |
||||||
|
</head> |
||||||
|
<body id="pad" class="light"> |
||||||
|
|
||||||
|
<div id="overlay"></div> |
||||||
|
|
||||||
|
<textarea id="writer" placeholder="Write..." class="{{.Post.Font}}" autofocus>{{if .Post.Title}}# {{.Post.Title}} |
||||||
|
|
||||||
|
{{end}}{{.Post.Content}}</textarea> |
||||||
|
|
||||||
|
<header id="tools"> |
||||||
|
<div id="clip"> |
||||||
|
{{if not .SingleUser}}<h1>{{if .Chorus}}<a href="/" title="Home">{{else}}<a href="/me/c/" title="View blogs">{{end}}{{.SiteName}}</a></h1>{{end}} |
||||||
|
<nav id="target" {{if .SingleUser}}style="margin-left:0"{{end}}><ul> |
||||||
|
<li>{{if .Blogs}}<a href="{{$c := index .Blogs 0}}{{$c.CanonicalURL}}">My Posts</a>{{else}}<a>Draft</a>{{end}}</li> |
||||||
|
</ul></nav> |
||||||
|
<span id="wc" class="hidden if-room room-4">0 words</span> |
||||||
|
</div> |
||||||
|
<noscript style="margin-left: 2em;"><strong>NOTE</strong>: for now, you'll need Javascript enabled to post.</noscript> |
||||||
|
<div id="belt"> |
||||||
|
{{if .Editing}}<div class="tool hidden if-room"><a href="{{if .EditCollection}}{{.EditCollection.CanonicalURL}}{{.Post.Slug}}/edit/meta{{else}}/{{if .SingleUser}}d/{{end}}{{.Post.Id}}/meta{{end}}" title="Edit post metadata" id="edit-meta"><img class="ic-24dp" src="/img/ic_info_dark@2x.png" /></a></div>{{end}} |
||||||
|
<div class="tool"><button title="Publish your writing" id="publish" style="font-weight: bold">Post</button></div> |
||||||
|
</div> |
||||||
|
</header> |
||||||
|
|
||||||
|
<script src="/js/h.js"></script> |
||||||
|
<script> |
||||||
|
var $writer = H.getEl('writer'); |
||||||
|
var $btnPublish = H.getEl('publish'); |
||||||
|
var $wc = H.getEl("wc"); |
||||||
|
var updateWordCount = function() { |
||||||
|
var words = 0; |
||||||
|
var val = $writer.el.value.trim(); |
||||||
|
if (val != '') { |
||||||
|
words = $writer.el.value.trim().replace(/\s+/gi, ' ').split(' ').length; |
||||||
|
} |
||||||
|
$wc.el.innerText = words + " word" + (words != 1 ? "s" : ""); |
||||||
|
}; |
||||||
|
var setButtonStates = function() { |
||||||
|
if (!canPublish) { |
||||||
|
$btnPublish.el.className = 'disabled'; |
||||||
|
return; |
||||||
|
} |
||||||
|
if ($writer.el.value.length === 0 || (draftDoc != 'lastDoc' && $writer.el.value == origDoc)) { |
||||||
|
$btnPublish.el.className = 'disabled'; |
||||||
|
} else { |
||||||
|
$btnPublish.el.className = ''; |
||||||
|
} |
||||||
|
}; |
||||||
|
{{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}'; |
||||||
|
var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}} |
||||||
|
H.load($writer, draftDoc, true); |
||||||
|
updateWordCount(); |
||||||
|
|
||||||
|
var typingTimer; |
||||||
|
var doneTypingInterval = 200; |
||||||
|
|
||||||
|
var posts; |
||||||
|
{{if and .Post.Id (not .Post.Slug)}} |
||||||
|
var token = null; |
||||||
|
var curPostIdx; |
||||||
|
posts = JSON.parse(H.get('posts', '[]')); |
||||||
|
for (var i=0; i<posts.length; i++) { |
||||||
|
if (posts[i].id == "{{.Post.Id}}") { |
||||||
|
token = posts[i].token; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
var canPublish = token != null; |
||||||
|
{{else}}var canPublish = true;{{end}} |
||||||
|
var publishing = false; |
||||||
|
var justPublished = false; |
||||||
|
|
||||||
|
var publish = function(content, font) { |
||||||
|
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}} |
||||||
|
if (!token) { |
||||||
|
alert("You don't have permission to update this post."); |
||||||
|
return; |
||||||
|
} |
||||||
|
{{end}} |
||||||
|
publishing = true; |
||||||
|
$btnPublish.el.textContent = 'Posting...'; |
||||||
|
$btnPublish.el.disabled = true; |
||||||
|
|
||||||
|
var http = new XMLHttpRequest(); |
||||||
|
var lang = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage); |
||||||
|
lang = lang.substring(0, 2); |
||||||
|
var post = H.getTitleStrict(content); |
||||||
|
|
||||||
|
var params = { |
||||||
|
body: post.content, |
||||||
|
title: post.title, |
||||||
|
font: font, |
||||||
|
lang: lang |
||||||
|
}; |
||||||
|
{{ if .Post.Slug }} |
||||||
|
var url = "/api/collections/{{.EditCollection.Alias}}/posts/{{.Post.Id}}"; |
||||||
|
{{ else if .Post.Id }} |
||||||
|
var url = "/api/posts/{{.Post.Id}}"; |
||||||
|
if (typeof token === 'undefined' || !token) { |
||||||
|
token = ""; |
||||||
|
} |
||||||
|
params.token = token; |
||||||
|
{{ else }} |
||||||
|
var url = "/api/posts"; |
||||||
|
var postTarget = '{{if .Blogs}}{{$c := index .Blogs 0}}{{$c.Alias}}{{else}}anonymous{{end}}'; |
||||||
|
if (postTarget != 'anonymous') { |
||||||
|
url = "/api/collections/" + postTarget + "/posts"; |
||||||
|
} |
||||||
|
{{ end }} |
||||||
|
|
||||||
|
http.open("POST", url, true); |
||||||
|
|
||||||
|
// Send the proper header information along with the request |
||||||
|
http.setRequestHeader("Content-type", "application/json"); |
||||||
|
|
||||||
|
http.onreadystatechange = function() { |
||||||
|
if (http.readyState == 4) { |
||||||
|
publishing = false; |
||||||
|
if (http.status == 200 || http.status == 201) { |
||||||
|
data = JSON.parse(http.responseText); |
||||||
|
id = data.data.id; |
||||||
|
nextURL = '{{if .SingleUser}}/d{{end}}/'+id; |
||||||
|
|
||||||
|
{{ if not .Post.Id }} |
||||||
|
// Post created |
||||||
|
if (postTarget != 'anonymous') { |
||||||
|
nextURL = {{if not .SingleUser}}'/'+postTarget+{{end}}'/'+data.data.slug; |
||||||
|
} |
||||||
|
editToken = data.data.token; |
||||||
|
|
||||||
|
{{ if not .User }}if (postTarget == 'anonymous') { |
||||||
|
// Save the data |
||||||
|
var posts = JSON.parse(H.get('posts', '[]')); |
||||||
|
|
||||||
|
{{if .Post.Id}}var newPost = H.createPost("{{.Post.Id}}", token, content); |
||||||
|
for (var i=0; i<posts.length; i++) { |
||||||
|
if (posts[i].id == "{{.Post.Id}}") { |
||||||
|
posts[i].title = newPost.title; |
||||||
|
posts[i].summary = newPost.summary; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
nextURL = "/pad/posts";{{else}}posts.push(H.createPost(id, editToken, content));{{end}} |
||||||
|
|
||||||
|
H.set('posts', JSON.stringify(posts)); |
||||||
|
} |
||||||
|
{{ end }} |
||||||
|
{{ end }} |
||||||
|
|
||||||
|
justPublished = true; |
||||||
|
if (draftDoc != 'lastDoc') { |
||||||
|
H.remove(draftDoc); |
||||||
|
{{if .Editing}}H.remove('draft{{.Post.Id}}font');{{end}} |
||||||
|
} else { |
||||||
|
H.set(draftDoc, ''); |
||||||
|
} |
||||||
|
|
||||||
|
{{if .EditCollection}} |
||||||
|
window.location = '{{.EditCollection.CanonicalURL}}{{.Post.Slug}}'; |
||||||
|
{{else}} |
||||||
|
window.location = nextURL; |
||||||
|
{{end}} |
||||||
|
} else { |
||||||
|
$btnPublish.el.textContent = 'Post'; |
||||||
|
alert("Failed to post. Please try again."); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
http.send(JSON.stringify(params)); |
||||||
|
}; |
||||||
|
|
||||||
|
setButtonStates(); |
||||||
|
$writer.on('keyup input', function() { |
||||||
|
setButtonStates(); |
||||||
|
clearTimeout(typingTimer); |
||||||
|
typingTimer = setTimeout(doneTyping, doneTypingInterval); |
||||||
|
}, false); |
||||||
|
$writer.on('keydown', function(e) { |
||||||
|
clearTimeout(typingTimer); |
||||||
|
if (e.keyCode == 13 && (e.metaKey || e.ctrlKey)) { |
||||||
|
$btnPublish.el.click(); |
||||||
|
} |
||||||
|
}); |
||||||
|
$btnPublish.on('click', function(e) { |
||||||
|
e.preventDefault(); |
||||||
|
if (!publishing && $writer.el.value) { |
||||||
|
var content = $writer.el.value; |
||||||
|
publish(content, selectedFont); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
WebFontConfig = { |
||||||
|
custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] } |
||||||
|
}; |
||||||
|
var selectedFont = H.get('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', '{{.Post.Font}}'); |
||||||
|
|
||||||
|
var doneTyping = function() { |
||||||
|
if (draftDoc == 'lastDoc' || $writer.el.value != origDoc) { |
||||||
|
H.save($writer, draftDoc); |
||||||
|
updateWordCount(); |
||||||
|
} |
||||||
|
}; |
||||||
|
window.addEventListener('beforeunload', function(e) { |
||||||
|
if (draftDoc != 'lastDoc' && $writer.el.value == origDoc) { |
||||||
|
H.remove(draftDoc); |
||||||
|
} else if (!justPublished) { |
||||||
|
doneTyping(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
try { |
||||||
|
(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) { |
||||||
|
// whatevs |
||||||
|
} |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html>{{end}} |
@ -0,0 +1,150 @@ |
|||||||
|
{{define "post"}}<!DOCTYPE HTML> |
||||||
|
<html {{if .Language.Valid}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}"> |
||||||
|
<head prefix="og: http://ogp.me/ns# article: http://ogp.me/ns/article#"> |
||||||
|
<meta charset="utf-8"> |
||||||
|
|
||||||
|
<title>{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{.Collection.DisplayTitle}}</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" /> |
||||||
|
<link rel="canonical" href="{{.CanonicalURL .Host}}" /> |
||||||
|
<meta name="generator" content="WriteFreely"> |
||||||
|
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}"> |
||||||
|
<meta name="description" content="{{.Summary}}"> |
||||||
|
{{if gt .Views 1}}<meta name="twitter:label1" value="Views"> |
||||||
|
<meta name="twitter:data1" value="{{largeNumFmt .Views}}">{{end}} |
||||||
|
<meta name="author" content="{{.Collection.Title}}" /> |
||||||
|
<meta itemprop="description" content="{{.Summary}}"> |
||||||
|
<meta itemprop="datePublished" content="{{.CreatedDate}}" /> |
||||||
|
<meta name="twitter:card" content="summary"> |
||||||
|
<meta name="twitter:description" content="{{.Summary}}"> |
||||||
|
<meta name="twitter:title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}"> |
||||||
|
{{if gt (len .Images) 0}}<meta name="twitter:image" content="{{index .Images 0}}">{{else}}<meta name="twitter:image" content="{{.Collection.AvatarURL}}">{{end}} |
||||||
|
<meta property="og:title" content="{{.PlainDisplayTitle}}" /> |
||||||
|
<meta property="og:description" content="{{.Summary}}" /> |
||||||
|
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" /> |
||||||
|
<meta property="og:type" content="article" /> |
||||||
|
<meta property="og:url" content="{{.CanonicalURL .Host}}" /> |
||||||
|
<meta property="og:updated_time" content="{{.Created8601}}" /> |
||||||
|
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}} |
||||||
|
<meta property="article:published_time" content="{{.Created8601}}"> |
||||||
|
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}} |
||||||
|
<style type="text/css"> |
||||||
|
body footer { |
||||||
|
max-width: 40rem; |
||||||
|
margin: 0 auto; |
||||||
|
} |
||||||
|
body#post header { |
||||||
|
padding: 1em 1rem; |
||||||
|
} |
||||||
|
article time.dt-published { |
||||||
|
display: block; |
||||||
|
color: #666; |
||||||
|
} |
||||||
|
body#post article h2#title{ |
||||||
|
margin-bottom: 0.5em; |
||||||
|
} |
||||||
|
article time.dt-published { |
||||||
|
margin-bottom: 1em; |
||||||
|
} |
||||||
|
</style> |
||||||
|
|
||||||
|
{{if .Collection.RenderMathJax}} |
||||||
|
<!-- Add mathjax logic --> |
||||||
|
{{template "mathjax" . }} |
||||||
|
{{end}} |
||||||
|
|
||||||
|
<!-- Add highlighting logic --> |
||||||
|
{{template "highlighting" .}} |
||||||
|
|
||||||
|
</head> |
||||||
|
<body id="post"> |
||||||
|
|
||||||
|
<div id="overlay"></div> |
||||||
|
|
||||||
|
{{template "user-navigation" .}} |
||||||
|
|
||||||
|
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time><div class="e-content">{{.HTMLContent}}</div></article> |
||||||
|
|
||||||
|
{{ if .Collection.ShowFooterBranding }} |
||||||
|
<footer dir="ltr"> |
||||||
|
<p style="text-align: left">Published by <a rel="author" href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a> |
||||||
|
{{ if .IsOwner }} · <span class="views" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span> |
||||||
|
· <a class="xtra-feature" href="/{{if not .SingleUser}}{{.Collection.Alias}}/{{end}}{{.Slug.String}}/edit" dir="{{.Direction}}">Edit</a> |
||||||
|
{{if .IsPinned}} · <a class="xtra-feature unpin" href="/{{.Collection.Alias}}/{{.Slug.String}}/unpin" dir="{{.Direction}}" onclick="unpinPost(event, '{{.ID}}')">Unpin</a>{{end}} |
||||||
|
{{ end }} |
||||||
|
</p> |
||||||
|
<nav> |
||||||
|
{{if .PinnedPosts}} |
||||||
|
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}} |
||||||
|
{{end}} |
||||||
|
</nav> |
||||||
|
<hr> |
||||||
|
<nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language.String}}</p></nav> |
||||||
|
</footer> |
||||||
|
{{ end }} |
||||||
|
</body> |
||||||
|
|
||||||
|
{{if .Collection.CanShowScript}} |
||||||
|
{{range .Collection.ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}} |
||||||
|
{{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}} |
||||||
|
{{end}} |
||||||
|
<script type="text/javascript"> |
||||||
|
|
||||||
|
var pinning = false; |
||||||
|
function unpinPost(e, postID) { |
||||||
|
e.preventDefault(); |
||||||
|
if (pinning) { |
||||||
|
return; |
||||||
|
} |
||||||
|
pinning = true; |
||||||
|
|
||||||
|
var $footer = document.getElementsByTagName('footer')[0]; |
||||||
|
var callback = function() { |
||||||
|
// Hide current page |
||||||
|
var $pinnedNavLink = $footer.getElementsByTagName('nav')[0].querySelector('.pinned.selected'); |
||||||
|
$pinnedNavLink.style.display = 'none'; |
||||||
|
}; |
||||||
|
|
||||||
|
var $pinBtn = $footer.getElementsByClassName('unpin')[0]; |
||||||
|
$pinBtn.innerHTML = '...'; |
||||||
|
|
||||||
|
var http = new XMLHttpRequest(); |
||||||
|
var url = "/api/collections/{{.Collection.Alias}}/unpin"; |
||||||
|
var params = [ { "id": postID } ]; |
||||||
|
http.open("POST", url, true); |
||||||
|
http.setRequestHeader("Content-type", "application/json"); |
||||||
|
http.onreadystatechange = function() { |
||||||
|
if (http.readyState == 4) { |
||||||
|
pinning = false; |
||||||
|
if (http.status == 200) { |
||||||
|
callback(); |
||||||
|
$pinBtn.style.display = 'none'; |
||||||
|
$pinBtn.innerHTML = 'Pin'; |
||||||
|
} else if (http.status == 409) { |
||||||
|
$pinBtn.innerHTML = 'Unpin'; |
||||||
|
} else { |
||||||
|
$pinBtn.innerHTML = 'Unpin'; |
||||||
|
alert("Failed to unpin." + (http.status>=500?" Please try again.":"")); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
http.send(JSON.stringify(params)); |
||||||
|
}; |
||||||
|
|
||||||
|
try { // Fonts |
||||||
|
WebFontConfig = { |
||||||
|
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans: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> |
||||||
|
</html>{{end}} |
@ -0,0 +1,230 @@ |
|||||||
|
{{define "collection"}}<!DOCTYPE HTML> |
||||||
|
<html {{if .Language}}lang="{{.Language}}"{{end}} dir="{{.Direction}}"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
|
||||||
|
<title>{{.DisplayTitle}}{{if not .SingleUser}} — {{.SiteName}}{{end}}</title> |
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/write.css" /> |
||||||
|
<link rel="shortcut icon" href="/favicon.ico" /> |
||||||
|
<link rel="canonical" href="{{.CanonicalURL}}"> |
||||||
|
{{if gt .CurrentPage 1}}<link rel="prev" href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">{{end}} |
||||||
|
{{if lt .CurrentPage .TotalPages}}<link rel="next" href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">{{end}} |
||||||
|
{{if not .IsPrivate}}<link rel="alternate" type="application/rss+xml" title="{{.DisplayTitle}} » Feed" href="{{.CanonicalURL}}feed/" />{{end}} |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||||
|
|
||||||
|
<meta name="generator" content="WriteFreely"> |
||||||
|
<meta name="description" content="{{.Description}}"> |
||||||
|
<meta itemprop="name" content="{{.DisplayTitle}}"> |
||||||
|
<meta itemprop="description" content="{{.Description}}"> |
||||||
|
<meta name="twitter:card" content="summary"> |
||||||
|
<meta name="twitter:title" content="{{.DisplayTitle}}"> |
||||||
|
<meta name="twitter:image" content="{{.AvatarURL}}"> |
||||||
|
<meta name="twitter:description" content="{{.Description}}"> |
||||||
|
<meta property="og:title" content="{{.DisplayTitle}}" /> |
||||||
|
<meta property="og:site_name" content="{{.DisplayTitle}}" /> |
||||||
|
<meta property="og:type" content="article" /> |
||||||
|
<meta property="og:url" content="{{.CanonicalURL}}" /> |
||||||
|
<meta property="og:description" content="{{.Description}}" /> |
||||||
|
<meta property="og:image" content="{{.AvatarURL}}"> |
||||||
|
{{if .StyleSheet}}<style type="text/css">{{.StyleSheetDisplay}}</style>{{end}} |
||||||
|
<style type="text/css"> |
||||||
|
body#collection header { |
||||||
|
max-width: 40em; |
||||||
|
margin: 1em auto; |
||||||
|
text-align: left; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
body#collection header.multiuser { |
||||||
|
max-width: 100%; |
||||||
|
margin: 1em; |
||||||
|
} |
||||||
|
body#collection header nav:not(.pinned-posts) { |
||||||
|
display: inline; |
||||||
|
} |
||||||
|
body#collection header nav.dropdown-nav, |
||||||
|
body#collection header nav.tabs, |
||||||
|
body#collection header nav.tabs a:first-child { |
||||||
|
margin: 0 0 0 1em; |
||||||
|
} |
||||||
|
</style> |
||||||
|
|
||||||
|
{{if .RenderMathJax}} |
||||||
|
<!-- Add mathjax logic --> |
||||||
|
{{template "mathjax" .}} |
||||||
|
{{end}} |
||||||
|
|
||||||
|
<!-- Add highlighting logic --> |
||||||
|
{{template "highlighting" . }} |
||||||
|
|
||||||
|
</head> |
||||||
|
<body id="collection" itemscope itemtype="http://schema.org/WebPage"> |
||||||
|
{{template "user-navigation" .}} |
||||||
|
|
||||||
|
<header> |
||||||
|
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1> |
||||||
|
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}} |
||||||
|
{{/*if not .Public/*}} |
||||||
|
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p--> |
||||||
|
{{/*end*/}} |
||||||
|
{{if .PinnedPosts}}<nav class="pinned-posts"> |
||||||
|
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL .Host}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav> |
||||||
|
{{end}} |
||||||
|
</header> |
||||||
|
|
||||||
|
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}} |
||||||
|
|
||||||
|
{{if .IsWelcome}} |
||||||
|
<div id="welcome"> |
||||||
|
<h2>Welcome, <strong>{{.Username}}</strong>!</h2> |
||||||
|
<p>This is your new blog.</p> |
||||||
|
<p><a class="simple-cta" href="/#{{.Alias}}">Start writing</a>, or <a class="simple-cta" href="/me/c/{{.Alias}}">customize</a> your blog.</p> |
||||||
|
<p>Check out our <a class="simple-cta" href="https://guides.write.as/writing/?pk_campaign=welcome">writing guide</a> to see what else you can do, and <a class="simple-cta" href="/contact">get in touch</a> anytime with questions or feedback.</p> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
|
||||||
|
{{template "posts" .}} |
||||||
|
|
||||||
|
{{if gt .TotalPages 1}}<nav id="paging" class="content-container clearfix"> |
||||||
|
{{if or (and .Format.Ascending (lt .CurrentPage .TotalPages)) (isRTL .Direction)}} |
||||||
|
{{if gt .CurrentPage 1}}<a href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">⇠ {{if and .Format.Ascending (lt .CurrentPage .TotalPages)}}Previous{{else}}Newer{{end}}</a>{{end}} |
||||||
|
{{if lt .CurrentPage .TotalPages}}<a style="float:right;" href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">{{if and .Format.Ascending (lt .CurrentPage .TotalPages)}}Next{{else}}Older{{end}} ⇢</a>{{end}} |
||||||
|
{{else}} |
||||||
|
{{if lt .CurrentPage .TotalPages}}<a href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">⇠ Older</a>{{end}} |
||||||
|
{{if gt .CurrentPage 1}}<a style="float:right;" href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">Newer ⇢</a>{{end}} |
||||||
|
{{end}} |
||||||
|
</nav>{{end}} |
||||||
|
|
||||||
|
{{if .Posts}}</section>{{else}}</div>{{end}} |
||||||
|
|
||||||
|
{{if .ShowFooterBranding }} |
||||||
|
<footer> |
||||||
|
<hr /> |
||||||
|
<nav dir="ltr"> |
||||||
|
{{if not .SingleUser}}<a class="home pubd" href="/">{{.SiteName}}</a> · {{end}}powered by <a style="margin-left:0" href="https://writefreely.org">writefreely</a> |
||||||
|
</nav> |
||||||
|
</footer> |
||||||
|
{{ end }} |
||||||
|
</body> |
||||||
|
|
||||||
|
{{if .CanShowScript}} |
||||||
|
{{range .ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}} |
||||||
|
{{if .Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}} |
||||||
|
{{end}} |
||||||
|
<script src="/js/h.js"></script> |
||||||
|
<script src="/js/postactions.js"></script> |
||||||
|
<script type="text/javascript"> |
||||||
|
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?')) { |
||||||
|
// AJAX |
||||||
|
deletePost(id, "", function() { |
||||||
|
// Remove post from list |
||||||
|
var $postEl = document.getElementById('post-' + id); |
||||||
|
$postEl.parentNode.removeChild($postEl); |
||||||
|
// TODO: add next post from this collection at the bottom |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
http.open("DELETE", url, true); |
||||||
|
http.onreadystatechange = function() { |
||||||
|
if (http.readyState == 4) { |
||||||
|
deleting = false; |
||||||
|
if (http.status == 204) { |
||||||
|
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." + (http.status>=500?" Please try again.":"")); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
http.send(); |
||||||
|
}; |
||||||
|
|
||||||
|
var pinning = false; |
||||||
|
function pinPost(e, postID, slug, title) { |
||||||
|
e.preventDefault(); |
||||||
|
if (pinning) { |
||||||
|
return; |
||||||
|
} |
||||||
|
pinning = true; |
||||||
|
|
||||||
|
var callback = function() { |
||||||
|
// Visibly remove post from collection |
||||||
|
var $postEl = document.getElementById('post-' + postID); |
||||||
|
$postEl.parentNode.removeChild($postEl); |
||||||
|
var $header = document.querySelector('header:not(.multiuser)'); |
||||||
|
var $pinnedNavs = $header.getElementsByTagName('nav'); |
||||||
|
// Add link to nav |
||||||
|
var link = '<a class="pinned" href="/{{.Alias}}/'+slug+'">'+title+'</a>'; |
||||||
|
if ($pinnedNavs.length == 0) { |
||||||
|
$header.insertAdjacentHTML("beforeend", '<nav>'+link+'</nav>'); |
||||||
|
} else { |
||||||
|
$pinnedNavs[0].insertAdjacentHTML("beforeend", link); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
var $pinBtn = document.getElementById('post-' + postID).getElementsByClassName('pin action')[0]; |
||||||
|
$pinBtn.innerHTML = '...'; |
||||||
|
|
||||||
|
var http = new XMLHttpRequest(); |
||||||
|
var url = "/api/collections/{{.Alias}}/pin"; |
||||||
|
var params = [ { "id": postID } ]; |
||||||
|
http.open("POST", url, true); |
||||||
|
http.setRequestHeader("Content-type", "application/json"); |
||||||
|
http.onreadystatechange = function() { |
||||||
|
if (http.readyState == 4) { |
||||||
|
pinning = false; |
||||||
|
if (http.status == 200) { |
||||||
|
callback(); |
||||||
|
} else if (http.status == 409) { |
||||||
|
$pinBtn.innerHTML = 'pin'; |
||||||
|
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 { |
||||||
|
$pinBtn.innerHTML = 'pin'; |
||||||
|
alert("Failed to pin." + (http.status>=500?" Please try again.":"")); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
http.send(JSON.stringify(params)); |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
WebFontConfig = { |
||||||
|
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans: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> |
||||||
|
</html>{{end}} |
@ -0,0 +1,32 @@ |
|||||||
|
{{define "invite-help"}} |
||||||
|
{{template "header" .}} |
||||||
|
<style> |
||||||
|
.copy-link { |
||||||
|
width: 100%; |
||||||
|
margin: 1rem 0; |
||||||
|
text-align: center; |
||||||
|
font-size: 1.2em; |
||||||
|
color: #555; |
||||||
|
} |
||||||
|
</style> |
||||||
|
<div class="snug content-container"> |
||||||
|
<h1>Invite to {{.SiteName}}</h1> |
||||||
|
{{ if .Expired }} |
||||||
|
<p style="font-style: italic">This invite link is expired.</p> |
||||||
|
{{ else }} |
||||||
|
<p>Copy the link below and send it to anyone that you want to join <em>{{ .SiteName }}</em>. You could paste it into an email, instant message, text message, or write it down on paper. Anyone who navigates to this special page will be able to create an account.</p> |
||||||
|
<input class="copy-link" type="text" name="invite-url" value="{{$.Host}}/invite/{{.Invite.ID}}" onfocus="if (this.select) this.select(); else this.setSelectionRange(0, this.value.length);" readonly /> |
||||||
|
<p> |
||||||
|
{{ if gt .Invite.MaxUses.Int64 0 }} |
||||||
|
{{if eq .Invite.MaxUses.Int64 1}}Only <strong>one</strong> user{{else}}Up to <strong>{{.Invite.MaxUses.Int64}}</strong> users{{end}} can sign up with this link. |
||||||
|
{{if gt .Invite.Uses 0}}So far, <strong>{{.Invite.Uses}}</strong> {{pluralize "person has" "people have" .Invite.Uses}} used it.{{end}} |
||||||
|
{{if .Invite.Expires}}It expires on <strong>{{.Invite.ExpiresFriendly}}</strong>.{{end}} |
||||||
|
{{ else }} |
||||||
|
It can be used as many times as you like{{if .Invite.Expires}} before <strong>{{.Invite.ExpiresFriendly}}</strong>, when it expires{{end}}. |
||||||
|
{{ end }} |
||||||
|
</p> |
||||||
|
{{ end }} |
||||||
|
</div> |
||||||
|
|
||||||
|
{{template "footer" .}} |
||||||
|
{{end}} |
Loading…
Reference in new issue