Add site-wide navigation on colls when chorus = true
This adds a new config value: `chorus` that signifies an instance is more about the Reader view than individual blogs / writers. When enabled, user navigation will show on all pages, including About, Reader, and Privacy (ref T680). It also uses different collection templates that keep the instance-wide navigation at the top of the page, instead of the author's name -- again, branded more for the collective than the individual. Ref T681
This commit is contained in:
parent
5f28eb55a5
commit
1a80cd3c02
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/writeas/web-core/data"
|
"github.com/writeas/web-core/data"
|
||||||
"github.com/writeas/web-core/log"
|
"github.com/writeas/web-core/log"
|
||||||
"github.com/writeas/writefreely/author"
|
"github.com/writeas/writefreely/author"
|
||||||
|
"github.com/writeas/writefreely/config"
|
||||||
"github.com/writeas/writefreely/page"
|
"github.com/writeas/writefreely/page"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -58,11 +59,15 @@ func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []str
|
|||||||
up.Flashes = flashes
|
up.Flashes = flashes
|
||||||
up.Path = r.URL.Path
|
up.Path = r.URL.Path
|
||||||
up.IsAdmin = u.IsAdmin()
|
up.IsAdmin = u.IsAdmin()
|
||||||
up.CanInvite = app.cfg.App.UserInvites != "" &&
|
up.CanInvite = canUserInvite(app.cfg, up.IsAdmin)
|
||||||
(up.IsAdmin || app.cfg.App.UserInvites != "admin")
|
|
||||||
return up
|
return up
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func canUserInvite(cfg *config.Config, isAdmin bool) bool {
|
||||||
|
return cfg.App.UserInvites != "" &&
|
||||||
|
(isAdmin || cfg.App.UserInvites != "admin")
|
||||||
|
}
|
||||||
|
|
||||||
func (up *UserPage) SetMessaging(u *User) {
|
func (up *UserPage) SetMessaging(u *User) {
|
||||||
//up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID)
|
//up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID)
|
||||||
}
|
}
|
||||||
|
2
app.go
2
app.go
@ -317,6 +317,8 @@ func pageForReq(app *App, r *http.Request) page.StaticPage {
|
|||||||
u = getUserSession(app, r)
|
u = getUserSession(app, r)
|
||||||
if u != nil {
|
if u != nil {
|
||||||
p.Username = u.Username
|
p.Username = u.Username
|
||||||
|
p.IsAdmin = u != nil && u.IsAdmin()
|
||||||
|
p.CanInvite = canUserInvite(app.cfg, p.IsAdmin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.CanViewReader = !app.cfg.App.Private || u != nil
|
p.CanViewReader = !app.cfg.App.Private || u != nil
|
||||||
|
@ -525,6 +525,8 @@ type CollectionPage struct {
|
|||||||
Username string
|
Username string
|
||||||
Collections *[]Collection
|
Collections *[]Collection
|
||||||
PinnedPosts *[]PublicPost
|
PinnedPosts *[]PublicPost
|
||||||
|
IsAdmin bool
|
||||||
|
CanInvite bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CollectionObj) ScriptDisplay() template.JS {
|
func (c *CollectionObj) ScriptDisplay() template.JS {
|
||||||
@ -737,6 +739,8 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||||||
IsCustomDomain: cr.isCustomDomain,
|
IsCustomDomain: cr.isCustomDomain,
|
||||||
IsWelcome: r.FormValue("greeting") != "",
|
IsWelcome: r.FormValue("greeting") != "",
|
||||||
}
|
}
|
||||||
|
displayPage.IsAdmin = u != nil && u.IsAdmin()
|
||||||
|
displayPage.CanInvite = canUserInvite(app.cfg, displayPage.IsAdmin)
|
||||||
var owner *User
|
var owner *User
|
||||||
if u != nil {
|
if u != nil {
|
||||||
displayPage.Username = u.Username
|
displayPage.Username = u.Username
|
||||||
@ -768,7 +772,11 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro
|
|||||||
// TODO: fix this mess of collections inside collections
|
// TODO: fix this mess of collections inside collections
|
||||||
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj)
|
displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj)
|
||||||
|
|
||||||
err = templates["collection"].ExecuteTemplate(w, "collection", displayPage)
|
collTmpl := "collection"
|
||||||
|
if app.cfg.App.Chorus {
|
||||||
|
collTmpl = "chorus-collection"
|
||||||
|
}
|
||||||
|
err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to render collection index: %v", err)
|
log.Error("Unable to render collection index: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -69,6 +69,7 @@ type (
|
|||||||
WebFonts bool `ini:"webfonts"`
|
WebFonts bool `ini:"webfonts"`
|
||||||
Landing string `ini:"landing"`
|
Landing string `ini:"landing"`
|
||||||
SimpleNav bool `ini:"simple_nav"`
|
SimpleNav bool `ini:"simple_nav"`
|
||||||
|
Chorus bool `ini:"chorus"`
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
SingleUser bool `ini:"single_user"`
|
SingleUser bool `ini:"single_user"`
|
||||||
|
@ -28,6 +28,8 @@ type StaticPage struct {
|
|||||||
Values map[string]string
|
Values map[string]string
|
||||||
Flashes []string
|
Flashes []string
|
||||||
CanViewReader bool
|
CanViewReader bool
|
||||||
|
IsAdmin bool
|
||||||
|
CanInvite bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizeHost alters the StaticPage to contain a real hostname. This is
|
// SanitizeHost alters the StaticPage to contain a real hostname. This is
|
||||||
|
11
posts.go
11
posts.go
@ -1345,15 +1345,24 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
|
|||||||
IsPinned bool
|
IsPinned bool
|
||||||
IsCustomDomain bool
|
IsCustomDomain bool
|
||||||
PinnedPosts *[]PublicPost
|
PinnedPosts *[]PublicPost
|
||||||
|
IsAdmin bool
|
||||||
|
CanInvite bool
|
||||||
}{
|
}{
|
||||||
PublicPost: p,
|
PublicPost: p,
|
||||||
StaticPage: pageForReq(app, r),
|
StaticPage: pageForReq(app, r),
|
||||||
IsOwner: cr.isCollOwner,
|
IsOwner: cr.isCollOwner,
|
||||||
IsCustomDomain: cr.isCustomDomain,
|
IsCustomDomain: cr.isCustomDomain,
|
||||||
}
|
}
|
||||||
|
tp.IsAdmin = u != nil && u.IsAdmin()
|
||||||
|
tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin)
|
||||||
tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll)
|
tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll)
|
||||||
tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p)
|
tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p)
|
||||||
if err := templates["collection-post"].ExecuteTemplate(w, "post", tp); err != nil {
|
|
||||||
|
postTmpl := "collection-post"
|
||||||
|
if app.cfg.App.Chorus {
|
||||||
|
postTmpl = "chorus-collection-post"
|
||||||
|
}
|
||||||
|
if err := templates[postTmpl].ExecuteTemplate(w, "post", tp); err != nil {
|
||||||
log.Error("Error in collection-post template: %v", err)
|
log.Error("Error in collection-post template: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
read.go
17
read.go
@ -48,6 +48,8 @@ type readPublication struct {
|
|||||||
CurrentPage int
|
CurrentPage int
|
||||||
TotalPages int
|
TotalPages int
|
||||||
SelTopic string
|
SelTopic string
|
||||||
|
IsAdmin bool
|
||||||
|
CanInvite bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func initLocalTimeline(app *App) {
|
func initLocalTimeline(app *App) {
|
||||||
@ -198,11 +200,16 @@ func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page in
|
|||||||
}
|
}
|
||||||
|
|
||||||
d := &readPublication{
|
d := &readPublication{
|
||||||
pageForReq(app, r),
|
StaticPage: pageForReq(app, r),
|
||||||
&posts,
|
Posts: &posts,
|
||||||
page,
|
CurrentPage: page,
|
||||||
ttlPages,
|
TotalPages: ttlPages,
|
||||||
tag,
|
SelTopic: tag,
|
||||||
|
}
|
||||||
|
if app.cfg.App.Chorus {
|
||||||
|
u := getUserSession(app, r)
|
||||||
|
d.IsAdmin = u != nil && u.IsAdmin()
|
||||||
|
d.CanInvite = canUserInvite(app.cfg, d.IsAdmin)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := templates["read"].ExecuteTemplate(w, "base", d)
|
err := templates["read"].ExecuteTemplate(w, "base", d)
|
||||||
|
@ -64,11 +64,14 @@ func initTemplate(parentDir, name string) {
|
|||||||
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
|
filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"),
|
||||||
filepath.Join(parentDir, templatesDir, "base.tmpl"),
|
filepath.Join(parentDir, templatesDir, "base.tmpl"),
|
||||||
}
|
}
|
||||||
if name == "collection" || name == "collection-tags" {
|
if name == "collection" || name == "collection-tags" || name == "chorus-collection" {
|
||||||
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl"
|
// These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl"
|
||||||
files = append(files, filepath.Join(parentDir, templatesDir, "include", "posts.tmpl"))
|
files = append(files, filepath.Join(parentDir, templatesDir, "include", "posts.tmpl"))
|
||||||
}
|
}
|
||||||
if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" {
|
if name == "chorus-collection" || name == "chorus-collection-post" {
|
||||||
|
files = append(files, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl"))
|
||||||
|
}
|
||||||
|
if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" || name == "chorus-collection" || name == "chorus-collection-post" {
|
||||||
files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl"))
|
files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl"))
|
||||||
}
|
}
|
||||||
templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...))
|
templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...))
|
||||||
|
@ -13,14 +13,38 @@
|
|||||||
<body {{template "body-attrs" .}}>
|
<body {{template "body-attrs" .}}>
|
||||||
<div id="overlay"></div>
|
<div id="overlay"></div>
|
||||||
<header>
|
<header>
|
||||||
<h2><a href="/">{{.SiteName}}</a></h2>
|
{{ if .SimpleNav }}<nav id="full-nav">
|
||||||
|
<div class="left-side">
|
||||||
|
<h2><a href="/">{{.SiteName}}</a></h2>
|
||||||
|
</div>
|
||||||
|
{{ else }}
|
||||||
|
<h2><a href="/">{{.SiteName}}</a></h2>
|
||||||
|
{{ end }}
|
||||||
{{if not .SingleUser}}
|
{{if not .SingleUser}}
|
||||||
<nav id="user-nav">
|
<nav id="user-nav">
|
||||||
|
{{if and .Chorus .Username}}
|
||||||
|
<nav class="dropdown-nav">
|
||||||
|
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
||||||
|
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
||||||
|
<li><a href="/me/settings">Account settings</a></li>
|
||||||
|
<li><a href="/me/export">Export</a></li>
|
||||||
|
{{if .CanInvite}}<li><a href="/me/invites">Invite people</a></li>{{end}}
|
||||||
|
<li class="separator"><hr /></li>
|
||||||
|
<li><a href="/me/logout">Log out</a></li>
|
||||||
|
</ul></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{{end}}
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<a href="/about"{{if eq .Path "/about"}} class="selected"{{end}}>About</a>
|
<a href="/about"{{if eq .Path "/about"}} class="selected"{{end}}>About</a>
|
||||||
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read"{{if eq .Path "/read"}} class="selected"{{end}}>Reader</a>{{end}}
|
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read"{{if eq .Path "/read"}} class="selected"{{end}}>Reader</a>{{end}}
|
||||||
{{if and (not .SingleUser) (not .Username)}}<a href="/login"{{if eq .Path "/login"}} class="selected"{{end}}>Log in</a>{{else if .SimpleNav}}<a href="/me/logout">Log out</a>{{end}}
|
{{if and (not .SingleUser) (not .Username)}}<a href="/login"{{if eq .Path "/login"}} class="selected"{{end}}>Log in</a>{{else if .SimpleNav}}<a href="/me/logout">Log out</a>{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
|
{{if .SimpleNav}}{{if .Username}}<div class="right-side" style="font-size: 0.86em;">
|
||||||
|
<a class="simple-btn" href="/new">New Post</a>
|
||||||
|
</div>{{end}}
|
||||||
|
</nav>
|
||||||
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
{{end}}
|
{{end}}
|
||||||
</header>
|
</header>
|
||||||
|
150
templates/chorus-collection-post.tmpl
Normal file
150
templates/chorus-collection-post.tmpl
Normal file
@ -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}}" />
|
||||||
|
<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}}" />
|
||||||
|
<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}}{{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 $header = document.getElementsByTagName('header')[0];
|
||||||
|
var callback = function() {
|
||||||
|
// Hide current page
|
||||||
|
var $pinnedNavLink = $header.getElementsByTagName('nav')[0].querySelector('.pinned.selected');
|
||||||
|
$pinnedNavLink.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
var $pinBtn = $header.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}}
|
230
templates/chorus-collection.tmpl
Normal file
230
templates/chorus-collection.tmpl
Normal file
@ -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}}{{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.getElementsByTagName('header')[0];
|
||||||
|
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}}
|
@ -65,11 +65,16 @@
|
|||||||
}
|
}
|
||||||
body#collection header nav {
|
body#collection header nav {
|
||||||
display: inline !important;
|
display: inline !important;
|
||||||
|
}
|
||||||
|
body#collection header nav:not(#full-nav):not(#user-nav) {
|
||||||
margin: 0 0 0 1em !important;
|
margin: 0 0 0 1em !important;
|
||||||
}
|
}
|
||||||
header nav#user-nav {
|
header nav#user-nav {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
body#collection header nav.tabs a:first-child {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{define "body-attrs"}}id="collection"{{end}}
|
{{define "body-attrs"}}id="collection"{{end}}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{{define "user-navigation"}}
|
{{define "user-navigation"}}
|
||||||
<header{{if .SingleUser}} class="singleuser"{{end}}>
|
<header class="{{if .SingleUser}}singleuser{{else}}multiuser{{end}}">
|
||||||
{{if .SingleUser}}
|
{{if .SingleUser}}
|
||||||
<nav id="user-nav">
|
<nav id="user-nav">
|
||||||
<nav class="dropdown-nav">
|
<nav class="dropdown-nav">
|
||||||
@ -30,6 +30,7 @@
|
|||||||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
|
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<nav id="user-nav">
|
<nav id="user-nav">
|
||||||
|
{{if .Username}}
|
||||||
<nav class="dropdown-nav">
|
<nav class="dropdown-nav">
|
||||||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
|
||||||
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
|
||||||
@ -41,20 +42,21 @@
|
|||||||
</ul></li>
|
</ul></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
{{end}}
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
{{if .SimpleNav}}
|
{{if .SimpleNav}}
|
||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read">Reader</a>{{end}}
|
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read">Reader</a>{{end}}
|
||||||
<a href="/me/logout">Log out</a>
|
{{if .Username}}<a href="/me/logout">Log out</a>{{else}}<a href="/login">Log in</a>{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>
|
<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>
|
<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
</nav>
|
</nav>
|
||||||
{{if .SimpleNav}}<div class="right-side">
|
{{if .SimpleNav}}{{if .Username}}<div class="right-side">
|
||||||
<a class="simple-btn" href="/new">New Post</a>
|
<a class="simple-btn" href="/new">New Post</a>
|
||||||
</div>
|
</div>{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user