summaryrefslogtreecommitdiff
path: root/src/http/tpl
diff options
context:
space:
mode:
Diffstat (limited to 'src/http/tpl')
-rw-r--r--src/http/tpl/admin.html18
-rw-r--r--src/http/tpl/assets.html57
-rw-r--r--src/http/tpl/base.html28
-rw-r--r--src/http/tpl/draft-posts.html50
-rw-r--r--src/http/tpl/edit-post.html142
-rw-r--r--src/http/tpl/finalize.html45
-rw-r--r--src/http/tpl/follow.html160
-rw-r--r--src/http/tpl/image.html5
-rw-r--r--src/http/tpl/index.html37
-rw-r--r--src/http/tpl/post.html44
-rw-r--r--src/http/tpl/posts.html47
-rw-r--r--src/http/tpl/redirect.html9
-rw-r--r--src/http/tpl/unsubscribe.html44
13 files changed, 686 insertions, 0 deletions
diff --git a/src/http/tpl/admin.html b/src/http/tpl/admin.html
new file mode 100644
index 0000000..f2ba4d6
--- /dev/null
+++ b/src/http/tpl/admin.html
@@ -0,0 +1,18 @@
+{{ define "body" }}
+
+<h1>Admin</h1>
+
+This is a directory of pages which are used for managing blog content. They are
+mostly left open to inspection, but you will not able to change
+anything without providing credentials.
+
+<ul>
+ <li><a href="{{ BlogURL "posts" }}">Posts</a></li>
+ <li><a href="{{ BlogURL "assets" }}">Assets</a></li>
+ <li><a href="{{ BlogURL "drafts" }}">Drafts</a> (private)</li>
+</ul>
+
+{{ end }}
+
+{{ template "base.html" . }}
+
diff --git a/src/http/tpl/assets.html b/src/http/tpl/assets.html
new file mode 100644
index 0000000..f21717a
--- /dev/null
+++ b/src/http/tpl/assets.html
@@ -0,0 +1,57 @@
+{{ define "body" }}
+
+<p>
+ <a href="{{ BlogURL "admin" }}">Back to Admin</a>
+</p>
+
+<h1>Assets</h1>
+
+<h2>Upload Asset</h2>
+
+<p>
+ If the given ID is the same as an existing asset's ID, then that asset will be
+ overwritten.
+</p>
+
+<form action="{{ BlogURL "assets/" }}" method="POST" enctype="multipart/form-data">
+ <div class="row">
+ <div class="four columns">
+ <input type="text" placeholder="Unique ID" name="id" />
+ </div>
+ <div class="four columns">
+ <input type="file" name="file" /><br/>
+ </div>
+ <div class="four columns">
+ <input type="submit" value="Upload" />
+ </div>
+ </div>
+</form>
+
+{{ if gt (len .Payload.IDs) 0 }}
+
+<h2>Existing Assets</h2>
+
+<table>
+
+ {{ range .Payload.IDs }}
+ <tr>
+ <td><a href="{{ AssetURL . }}">{{ . }}</a></td>
+ <td>
+ <form
+ action="{{ AssetURL . }}?method=delete"
+ method="POST"
+ style="margin-bottom: 0;"
+ >
+ <input type="submit" value="Delete" />
+ </form>
+ </td>
+ </tr>
+ {{ end }}
+
+</table>
+
+{{ end }}
+
+{{ end }}
+
+{{ template "base.html" . }}
diff --git a/src/http/tpl/base.html b/src/http/tpl/base.html
new file mode 100644
index 0000000..f286222
--- /dev/null
+++ b/src/http/tpl/base.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+
+ <head>
+ <style>{{ StaticInlineCSS "new.css" }}</style>
+ <style>{{ StaticInlineCSS "mediocre.css" }}</style>
+ </head>
+
+ <body>
+
+ <header>
+ <a href="{{ BlogURL "/" }}"><strong>Mediocre Blog</strong></a>
+ by
+ <a href="https://mediocregopher.com">mediocregopher</a>
+ &nbsp;&nbsp;&nbsp;//&nbsp;&nbsp;&nbsp;
+ <a href="{{ BlogURL "follow" }}">Follow</a>
+ &nbsp;/&nbsp;
+ <a href="{{ BlogURL "feed.xml" }}">RSS</a>
+ &nbsp;/&nbsp;
+ <a href="{{ StaticURL "wtfpl.txt" }}">License</a>
+ </header>
+
+ {{ template "body" . }}
+
+ </body>
+
+</html>
+
diff --git a/src/http/tpl/draft-posts.html b/src/http/tpl/draft-posts.html
new file mode 100644
index 0000000..53261b9
--- /dev/null
+++ b/src/http/tpl/draft-posts.html
@@ -0,0 +1,50 @@
+{{ define "body" }}
+
+ <p>
+ <a href="{{ BlogURL "admin" }}">Back to Admin</a>
+ </p>
+
+ <h1>Drafts</h1>
+
+ <p>
+ <a href="{{ BlogURL "drafts/" }}?edit">New Draft</a>
+ </p>
+
+ {{ if ge .Payload.PrevPage 0 }}
+ <p>
+ <a href="?p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a>
+ </p>
+ {{ end }}
+
+ <table>
+
+ {{ range .Payload.Posts }}
+ <tr>
+ <td><a href="{{ DraftURL .ID }}">{{ .Title }}</a></td>
+ <td>
+ <a href="{{ DraftURL .ID }}?edit">
+ Edit
+ </a>
+ </td>
+ <td>
+ <form
+ action="{{ DraftURL .ID }}?method=delete"
+ method="POST"
+ >
+ <input type="submit" value="Delete" />
+ </form>
+ </td>
+ </tr>
+ {{ end }}
+
+ </table>
+
+ {{ if ge .Payload.NextPage 0 }}
+ <p>
+ <a href="?p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a>
+ </p>
+ {{ end }}
+
+{{ end }}
+
+{{ template "base.html" . }}
diff --git a/src/http/tpl/edit-post.html b/src/http/tpl/edit-post.html
new file mode 100644
index 0000000..f8e2730
--- /dev/null
+++ b/src/http/tpl/edit-post.html
@@ -0,0 +1,142 @@
+{{ define "body" }}
+
+<p>
+ {{ if .Payload.IsDraft }}
+ <a href="{{ BlogURL "drafts/" }}">
+ Back to Drafts
+ </a>
+ {{ else }}
+ <a href="{{ BlogURL "posts/" }}">
+ Back to Posts
+ </a>
+ {{ end }}
+</p>
+
+<form method="POST" action="{{ BlogURL "posts/" }}">
+
+ <table>
+
+ <tr>
+ <td>
+ Unique ID
+ </td>
+ <td>
+ {{ if eq .Payload.Post.ID "" }}
+ <input
+ name="id"
+ type="text"
+ placeholder="e.g. how-to-fly-a-kite"
+ value="{{ .Payload.Post.ID }}" />
+ {{ else if .Payload.IsDraft }}
+ {{ .Payload.Post.ID }}
+ <input name="id" type="hidden" value="{{ .Payload.Post.ID }}" />
+ {{ else }}
+ <a href="{{ PostURL .Payload.Post.ID }}">{{ .Payload.Post.ID }}</a>
+ <input name="id" type="hidden" value="{{ .Payload.Post.ID }}" />
+ {{ end }}
+ </td>
+ </tr>
+
+ <tr>
+ <td>Tags (space separated)</td>
+ <td>
+ <input
+ name="tags"
+ type="text"
+ value="{{- range $i, $tag := .Payload.Post.Tags -}}
+ {{- if ne $i 0 }} {{ end }}{{ $tag -}}
+ {{- end -}}
+ "/>
+
+ {{ if gt (len .Payload.Tags) 0 }}
+ <em>
+ Existing tags:
+ {{ range $i, $tag := .Payload.Tags }}
+ {{ if ne $i 0 }} {{ end }}{{ $tag }}
+ {{ end }}
+ </em>
+ {{ end }}
+ </td>
+ </tr>
+
+ <tr>
+ <td>Series</td>
+ <td>
+ <input
+ name="series"
+ type="text"
+ value="{{ .Payload.Post.Series }}" />
+ </td>
+ </tr>
+
+ <tr>
+ <td>Title</td>
+ <td>
+ <input
+ name="title"
+ type="text"
+ value="{{ .Payload.Post.Title }}" />
+ </td>
+ </tr>
+
+ <tr>
+ <td>Description</td>
+ <td>
+ <input
+ name="description"
+ type="text"
+ value="{{ .Payload.Post.Description }}" />
+ </td>
+ </tr>
+
+ </table>
+
+ <p>
+ <textarea
+ name="body"
+ placeholder="Blog body"
+ style="width:100%;height: 75vh;"
+ >
+ {{- .Payload.Post.Body -}}
+ </textarea>
+ </p>
+
+ <p>
+
+ <input
+ type="submit"
+ value="Preview"
+ formaction="{{ BlogURL "posts/" }}{{ .Payload.Post.ID }}?method=preview"
+ formtarget="_blank"
+ />
+
+ {{ if .Payload.IsDraft }}
+ <input type="submit" value="Save" formaction="{{ BlogURL "drafts/" }}" />
+
+
+ <script>
+ function confirmPublish(event) {
+ if (!confirm("Are you sure you're ready to publish?"))
+ event.preventDefault();
+ }
+ </script>
+
+
+ <input
+ type="submit"
+ value="Publish"
+ formaction="{{ BlogURL "posts/" }}"
+ onclick="confirmPublish(event)"
+ />
+
+ {{ else }}
+ <input type="submit" value="Update" formaction="{{ BlogURL "posts/" }}" />
+ {{ end }}
+
+ </p>
+
+</form>
+
+{{ end }}
+
+{{ template "base.html" . }}
diff --git a/src/http/tpl/finalize.html b/src/http/tpl/finalize.html
new file mode 100644
index 0000000..8bdfceb
--- /dev/null
+++ b/src/http/tpl/finalize.html
@@ -0,0 +1,45 @@
+{{ define "body" }}
+
+<script async type="module" src="{{ StaticURL "api.js" }}"></script>
+
+<style>
+#result.success { color: green; }
+#result.fail { color: red; }
+</style>
+
+<span id="result"></span>
+
+<script>
+
+(async () => {
+
+ const resultSpan = document.getElementById("result");
+
+ try {
+
+ const urlParams = new URLSearchParams(window.location.search);
+ const subToken = urlParams.get('subToken');
+
+ if (!subToken) throw "No subscription token provided";
+
+ const api = await import("{{ StaticURL "api.js" }}");
+
+ await api.call('/api/mailinglist/finalize', {
+ body: { subToken },
+ });
+
+ resultSpan.className = "success";
+ resultSpan.innerHTML = "Your email subscription has been finalized! Please go on about your day.";
+
+ } catch (e) {
+ resultSpan.className = "fail";
+ resultSpan.innerHTML = e;
+ }
+
+})();
+
+</script>
+
+{{ end }}
+
+{{ template "base.html" . }}
diff --git a/src/http/tpl/follow.html b/src/http/tpl/follow.html
new file mode 100644
index 0000000..23c30a6
--- /dev/null
+++ b/src/http/tpl/follow.html
@@ -0,0 +1,160 @@
+{{ define "body" }}
+
+<script async type="module" src="{{ StaticURL "api.js" }}"></script>
+
+<p>
+ Here's your options for receiving updates about new blog posts:
+</p>
+
+<h2>Option 1: Email</h2>
+
+<p>
+ Email is by far my preferred option for notifying followers of new posts.
+</p>
+
+<p>
+ The entire email list system for this blog, from storing subscriber email
+ addresses to the email server which sends the notifications out, has been
+ designed from scratch and is completely self-hosted in my living room.
+</p>
+
+<p>
+ I solemnly swear that:
+</p>
+
+<ul>
+
+ <li>
+ You will never receive an email from this blog except to notify of a new
+ post.
+ </li>
+
+ <li>
+ Your email will never be provided or sold to anyone else for any reason.
+ </li>
+
+</ul>
+
+<p>
+ With all that said, if you'd like to receive an email everytime a new blog
+ post is published then input your email below and smash that subscribe button!
+ You will need to verify your email, so be sure to check your spam folder to
+ complete the process if you don't immediately see anything in your inbox.
+</p>
+
+<p style="color: var(--nc-ac-1);">
+ Unfortunately Google considers all emails from my mail server to be spam. I'm
+ tired of seeing the bounce errors on my side, so I'm disabling the ability to
+ sign up for the mailing list with a GMail address. Sorry (not sorry).
+</p>
+
+<style>
+
+#emailStatus.success {
+ color: green;
+}
+
+#emailStatus.fail {
+ color: red;
+}
+
+</style>
+
+<form action="javascript:void(0);">
+ <input type="email" placeholder="name@host.com" id="emailAddress" />
+ <input class="button-primary" type="submit" value="Subscribe" id="emailSubscribe" />
+ <span id="emailStatus"></span>
+</form>
+
+<script>
+
+const emailAddress = document.getElementById("emailAddress");
+const emailSubscribe = document.getElementById("emailSubscribe");
+const emailSubscribeOrigValue = emailSubscribe.value;
+const emailStatus = document.getElementById("emailStatus");
+
+emailSubscribe.onclick = async () => {
+
+ const api = await import("{{ StaticURL "api.js" }}");
+
+ emailSubscribe.disabled = true;
+ emailSubscribe.className = "";
+ emailSubscribe.value = "Please hold...";
+ emailStatus.innerHTML = '';
+
+ try {
+
+ if (!window.isSecureContext) {
+ throw "The browser environment is not secure.";
+ }
+
+ await api.call('/api/mailinglist/subscribe', {
+ body: { email: emailAddress.value },
+ requiresPow: true,
+ });
+
+ emailStatus.className = "success";
+ emailStatus.innerHTML = "Verification email sent (check your spam folder)";
+
+ } catch (e) {
+ emailStatus.className = "fail";
+ emailStatus.innerHTML = e;
+
+ } finally {
+ emailSubscribe.disabled = false;
+ emailSubscribe.className = "button-primary";
+ emailSubscribe.value = emailSubscribeOrigValue;
+ }
+
+};
+
+</script>
+
+<h2>Option 2: RSS</h2>
+
+<p>
+ RSS is the classic way to follow any blog. It comes from a time before
+ aggregators like reddit and twitter stole the show, when people felt capable
+ to manage their own content feeds. We should use it again.
+</p>
+
+<p>
+ To follow over RSS give any RSS reader the following URL...
+</p>
+
+<p>
+ <a href="{{ BlogURL "feed.xml" }}">{{ BlogURL "feed.xml" }}</a>
+</p>
+
+<p>
+ ...and posts from this blog will show up in your RSS feed as soon as they are
+ published. There are literally thousands of RSS readers out there. Here's some
+ recommendations:
+</p>
+
+<ul>
+ <li>
+ <a href="https://chrome.google.com/webstore/detail/rss-feed-reader/pnjaodmkngahhkoihejjehlcdlnohgmp">
+ Google Chrome Browser Extension
+ </a>
+ </li>
+
+ <li>
+ <a href="https://f-droid.org/en/packages/net.etuldan.sparss.floss/">
+ spaRSS
+ </a>
+ is my preferred android RSS reader, but you'll need to install
+ <a href="https://f-droid.org/">f-droid</a> on your device to use it (a
+ good thing to do anyway, imo).
+ </li>
+
+ <li>
+ <a href="https://ranchero.com/netnewswire/">NetNewsWire</a>
+ is a good reader for iPhone/iPad/Mac devices, so I'm told. Their homepage
+ description makes a much better sales pitch for RSS than I ever could.
+ </li>
+</ul>
+
+{{ end }}
+
+{{ template "base.html" . }}
diff --git a/src/http/tpl/image.html b/src/http/tpl/image.html
new file mode 100644
index 0000000..ba9b75d
--- /dev/null
+++ b/src/http/tpl/image.html
@@ -0,0 +1,5 @@
+<div style="text-align: center;">
+ <a href="{{ AssetURL .ID }}" target="_blank">
+ <img src="{{ AssetURL .ID }}{{ if .Resizable }}?w=800{{ end }}" />
+ </a>
+</div>
diff --git a/src/http/tpl/index.html b/src/http/tpl/index.html
new file mode 100644
index 0000000..ce5f264
--- /dev/null
+++ b/src/http/tpl/index.html
@@ -0,0 +1,37 @@
+{{ define "body" }}
+
+ {{ if ge .Payload.PrevPage 0 }}
+ <p>
+ <a href="?p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a>
+ </p>
+ {{ else }}
+ <p>
+ Welcome to the Mediocre Blog! Posts are listed in chronological order. If
+ you aren't sure of where to start I recommend picking at random.
+ </p>
+ {{ end }}
+
+ <table>
+
+ <colgroup>
+ <col span="1" style="width: 5rem;">
+ </colgroup>
+
+ {{ range .Payload.Posts }}
+ <tr>
+ <td>{{ DateTimeFormat .PublishedAt }}</td>
+ <td><a href="{{ PostURL .ID }}">{{ .Title }}</td>
+ <td><em>{{ .Description }}</em></td>
+ </tr>
+ {{ end }}
+ </table>
+
+ {{ if ge .Payload.NextPage 0 }}
+ <p>
+ <a href="?p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a>
+ </p>
+ {{ end }}
+
+{{ end }}
+
+{{ template "base.html" . }}
diff --git a/src/http/tpl/post.html b/src/http/tpl/post.html
new file mode 100644
index 0000000..23500eb
--- /dev/null
+++ b/src/http/tpl/post.html
@@ -0,0 +1,44 @@
+{{ define "body" }}
+
+<h1 id="post-headline">
+ {{ .Payload.Title }}
+</h1>
+
+<p>
+ <em>- {{ .Payload.Description }}</em>
+</p>
+
+<hr/>
+
+{{ .Payload.Body }}
+
+<p><em>
+ Published {{ DateTimeFormat .Payload.PublishedAt }}
+ {{ if not .Payload.LastUpdatedAt.IsZero }}
+ <br/>Last updated {{ DateTimeFormat .Payload.LastUpdatedAt }}
+ {{ end }}
+</em></p>
+
+{{ if (or .Payload.SeriesPrevious .Payload.SeriesNext) }}
+<hr/>
+<p><em>
+ This post is part of a series.<br/>
+
+ {{ if .Payload.SeriesPrevious }}
+ Previously: <a href="{{ PostURL .Payload.SeriesPrevious.ID }}">{{ .Payload.SeriesPrevious.Title }}</a>
+ {{ end }}
+
+ {{ if (and .Payload.SeriesNext .Payload.SeriesPrevious) }}
+ </br>
+ {{ end }}
+
+ {{ if .Payload.SeriesNext }}
+ Next: <a href="{{ PostURL .Payload.SeriesNext.ID }}">{{ .Payload.SeriesNext.Title }}</a></br>
+ {{ end }}
+
+</em></p>
+{{ end }}
+
+{{ end }}
+
+{{ template "base.html" . }}
diff --git a/src/http/tpl/posts.html b/src/http/tpl/posts.html
new file mode 100644
index 0000000..fbeaa41
--- /dev/null
+++ b/src/http/tpl/posts.html
@@ -0,0 +1,47 @@
+{{ define "body" }}
+
+ <p>
+ <a href="{{ BlogURL "admin" }}">Back to Admin</a>
+ </p>
+
+ <h1>Posts</h1>
+
+ {{ if ge .Payload.PrevPage 0 }}
+ <p>
+ <a href="?p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a>
+ </p>
+ {{ end }}
+
+ <table>
+
+ {{ range .Payload.Posts }}
+ <tr>
+ <td>{{ .PublishedAt.Local.Format "2006-01-02 15:04:05 MST" }}</td>
+ <td><a href="{{ PostURL .ID }}">{{ .Title }}</a></td>
+ <td>
+ <a href="{{ PostURL .ID }}?edit">
+ Edit
+ </a>
+ </td>
+ <td>
+ <form
+ action="{{ PostURL .ID }}?method=delete"
+ method="POST"
+ >
+ <input type="submit" value="Delete" />
+ </form>
+ </td>
+ </tr>
+ {{ end }}
+
+ </table>
+
+ {{ if ge .Payload.NextPage 0 }}
+ <p>
+ <a href="?p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a>
+ </p>
+ {{ end }}
+
+{{ end }}
+
+{{ template "base.html" . }}
diff --git a/src/http/tpl/redirect.html b/src/http/tpl/redirect.html
new file mode 100644
index 0000000..a50b324
--- /dev/null
+++ b/src/http/tpl/redirect.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="refresh" content="0; url='{{ .Payload.URL }}'" />
+ </head>
+ <body>
+ <p>Redirecting...</p>
+ </body>
+</html>
diff --git a/src/http/tpl/unsubscribe.html b/src/http/tpl/unsubscribe.html
new file mode 100644
index 0000000..ad01735
--- /dev/null
+++ b/src/http/tpl/unsubscribe.html
@@ -0,0 +1,44 @@
+{{ define "body" }}
+
+<script async type="module" src="{{ StaticURL "api.js" }}"></script>
+
+<style>
+#result.success { color: green; }
+#result.fail { color: red; }
+</style>
+
+<span id="result"></span>
+
+<script>
+
+(async () => {
+
+ const resultSpan = document.getElementById("result");
+
+ try {
+ const urlParams = new URLSearchParams(window.location.search);
+ const unsubToken = urlParams.get('unsubToken');
+
+ if (!unsubToken) throw "No unsubscribe token provided";
+
+ const api = await import("{{ StaticURL "api.js" }}");
+
+ await api.call('/api/mailinglist/unsubscribe', {
+ body: { unsubToken },
+ });
+
+ resultSpan.className = "success";
+ resultSpan.innerHTML = "You have been unsubscribed! Please go on about your day.";
+
+ } catch (e) {
+ resultSpan.className = "fail";
+ resultSpan.innerHTML = e;
+ }
+
+})();
+
+</script>
+
+{{ end }}
+
+{{ template "base.html" . }}