diff --git a/admin/blog/blog.go b/admin/blog/blog.go
index 322b7db..806f34c 100644
--- a/admin/blog/blog.go
+++ b/admin/blog/blog.go
@@ -15,12 +15,8 @@ func Handler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mux := http.NewServeMux()
- mux.Handle("/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- blogID := r.PathValue("id")
- w.Write([]byte(blogID))
- }))
-
- mux.Handle("/", handleBlogIndex(app))
+ mux.Handle("/{id}", serveBlogPost(app))
+ mux.Handle("/", serveBlogIndex(app))
mux.ServeHTTP(w, r)
})
@@ -38,14 +34,9 @@ type (
}
)
-func handleBlogIndex(app *model.AppState) http.Handler {
+func serveBlogIndex(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- session, err := controller.GetSessionFromRequest(app, r)
- if err != nil {
- fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
+ session := r.Context().Value("session").(*model.Session)
posts, err := controller.GetBlogPosts(app.DB, false, -1, 0)
if err != nil {
@@ -108,3 +99,35 @@ func handleBlogIndex(app *model.AppState) http.Handler {
}
})
}
+
+func serveBlogPost(app *model.AppState) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ session := r.Context().Value("session").(*model.Session)
+
+ blogID := r.PathValue("id")
+
+ post, err := controller.GetBlogPost(app.DB, blogID)
+ if err != nil {
+ fmt.Printf("can't find blog with ID %s\n", blogID)
+ http.NotFound(w, r)
+ return
+ }
+
+ type blogPostData struct {
+ core.AdminPageData
+ Post *model.BlogPost
+ }
+
+ err = templates.EditBlogTemplate.Execute(w, blogPostData{
+ AdminPageData: core.AdminPageData{
+ Session: session,
+ },
+ Post: post,
+ })
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Error rendering admin edit page for blog %s: %v\n", blogID, err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+ })
+}
diff --git a/admin/static/edit-blog.css b/admin/static/edit-blog.css
new file mode 100644
index 0000000..86626d6
--- /dev/null
+++ b/admin/static/edit-blog.css
@@ -0,0 +1,100 @@
+input[type="text"] {
+ padding: .3em .5em;
+ font-size: inherit;
+ font-family: inherit;
+ border: none;
+ border-radius: 4px;
+ outline: none;
+ color: inherit;
+ background-color: var(--bg-1);
+ box-shadow: var(--shadow-sm);
+}
+
+#blogpost {
+ margin-bottom: 1em;
+ padding: 1.5em;
+
+ border-radius: 8px;
+ background-color: var(--bg-2);
+ box-shadow: var(--shadow-lg);
+
+ transition: background .1s ease-out, color .1s ease-out;
+}
+
+#blogpost label {
+ margin: 1.2em 0 .2em .1em;
+ display: block;
+ font-size: .8em;
+ text-transform: uppercase;
+ font-weight: 600;
+}
+#blogpost label:first-of-type {
+ margin-top: 0;
+}
+
+#blogpost h2 {
+ margin: 0;
+ font-size: 2em;
+}
+
+#blogpost #title {
+ width: 100%;
+ margin: 0 -.2em;
+ padding: 0 .2em;
+ resize: none;
+ font-family: inherit;
+ font-size: inherit;
+ font-weight: bold;
+ border-radius: 4px;
+ border: 1px solid transparent;
+ background: transparent;
+ color: var(--fg-3);
+ outline: none;
+ cursor: pointer;
+ transition: background .1s ease-out, border-color .1s ease-out;
+
+ /*position: relative; outline: none;*/
+ white-space: pre-wrap; overflow-wrap: break-word;
+}
+
+#blogpost #title:hover {
+ background-color: var(--bg-3);
+ border-color: var(--fg-0);
+}
+
+#blogpost #title:active,
+#blogpost #title:focus {
+ background-color: var(--bg-3);
+}
+
+#blogpost textarea {
+ width: calc(100% - 2em);
+ margin: 0;
+ padding: 1em;
+ display: block;
+ border: none;
+ border-radius: 4px;
+ background-color: var(--bg-1);
+ color: var(--fg-3);
+ box-shadow: var(--shadow-md);
+ resize: vertical;
+ outline: none;
+}
+
+#blogpost #description {
+ font-family: inherit;
+}
+
+#blogpost select {
+ padding: .5em .8em;
+ font-size: inherit;
+ border: none;
+ border-radius: 10em;
+ color: var(--fg-3);
+ background-color: var(--bg-1);
+ box-shadow: var(--shadow-sm);
+}
+
+#blogpost .blog-actions {
+ margin-top: 1em;
+}
diff --git a/admin/static/edit-blog.js b/admin/static/edit-blog.js
new file mode 100644
index 0000000..642c69c
--- /dev/null
+++ b/admin/static/edit-blog.js
@@ -0,0 +1,63 @@
+const blogID = document.getElementById("blogpost").dataset.id;
+const titleInput = document.getElementById("title");
+const descInput = document.getElementById("description");
+const mdInput = document.getElementById("markdown");
+const blueskyActorInput = document.getElementById("bluesky-actor");
+const blueskyPostInput = document.getElementById("bluesky-post");
+const visInput = document.getElementById("visibility");
+const saveBtn = document.getElementById("save");
+const deleteBtn = document.getElementById("delete");
+
+saveBtn.addEventListener("click", () => {
+ fetch("/api/v1/blog/" + blogID, {
+ method: "PUT",
+ body: JSON.stringify({
+ title: titleInput.value,
+ description: descInput.value,
+ markdown: mdInput.value,
+ bluesky_actor: blueskyActorInput.value,
+ bluesky_post: blueskyPostInput.value,
+ visible: visInput.value === "true",
+ }),
+ headers: { "Content-Type": "application/json" }
+ }).then(res => {
+ if (!res.ok) {
+ res.text().then(error => {
+ console.error(error);
+ alert("Failed to update blog post: " + error);
+ });
+ return;
+ }
+
+ location = location;
+ });
+});
+
+deleteBtn.addEventListener("click", () => {
+ if (blogID != prompt(
+ "You are about to permanently delete " + blogID + ". " +
+ "This action is irreversible. " +
+ "Please enter \"" + blogID + "\" to continue.")) return;
+ fetch("/api/v1/blog/" + blogID, {
+ method: "DELETE",
+ }).then(res => {
+ if (!res.ok) {
+ res.text().then(error => {
+ console.error(error);
+ alert("Failed to delete blog post: " + error);
+ });
+ return;
+ }
+
+ location = "/admin";
+ });
+});
+
+[titleInput, descInput, mdInput, blueskyActorInput, blueskyPostInput, visInput].forEach(input => {
+ input.addEventListener("change", () => {
+ saveBtn.disabled = false;
+ });
+ input.addEventListener("keypress", () => {
+ saveBtn.disabled = false;
+ });
+});
diff --git a/admin/templates/html/edit-blog.html b/admin/templates/html/edit-blog.html
new file mode 100644
index 0000000..71570ee
--- /dev/null
+++ b/admin/templates/html/edit-blog.html
@@ -0,0 +1,87 @@
+{{define "head"}}
+
Editing {{.Post.Title}} - ari melody 💫
+
+
+{{end}}
+
+{{define "content"}}
+
+ Editing Blog Post
+
+
+
+
+
{{.Post.Title}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Clicking the button below will delete this blog post.
+ This action is irreversible.
+ You will be prompted to confirm this decision.
+
+
+
+
+
+
+{{end}}
diff --git a/admin/templates/html/edit-release.html b/admin/templates/html/edit-release.html
index df3c48a..1ca31fe 100644
--- a/admin/templates/html/edit-release.html
+++ b/admin/templates/html/edit-release.html
@@ -8,7 +8,7 @@
{{define "content"}}
- Editing {{.Release.Title}}
+ Editing Release
diff --git a/admin/templates/templates.go b/admin/templates/templates.go
index 23632b7..6244540 100644
--- a/admin/templates/templates.go
+++ b/admin/templates/templates.go
@@ -200,3 +200,12 @@ var BlogsTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
componentBlogPostHTML,
}, "\n"),
))
+
+//go:embed "html/edit-blog.html"
+var editBlogHTML string
+var EditBlogTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(
+ strings.Join([]string{
+ editBlogHTML,
+ componentBlogPostHTML,
+ }, "\n"),
+))
diff --git a/model/blog.go b/model/blog.go
index d341154..4a2d03a 100644
--- a/model/blog.go
+++ b/model/blog.go
@@ -21,7 +21,7 @@ type (
Markdown string `db:"markdown"`
HTML template.HTML `db:"html"`
BlueskyActorID *string `db:"bluesky_actor"`
- BlueskyPostID *string `db:"bluesky_post"`
+ BlueskyPostID *string `db:"bluesky_post"`
}
)